From 99c79e92ac138255a903585e5e8bb9d049adc8e7 Mon Sep 17 00:00:00 2001 From: Devin Neal <devin@eulertour.com> Date: Fri, 1 Jan 2021 19:10:23 -0800 Subject: [PATCH] Release v0.2.0 (#897) Changes since v0.1.1. --- .gitattributes | 1 + docker/Dockerfile | 3 - docs/source/changelog.rst | 100 ++- docs/source/examples.rst | 82 +- docs/source/installation/mac.rst | 10 - docs/source/installation/troubleshooting.rst | 38 +- docs/source/installation/win.rst | 26 - docs/source/reference.rst | 1 + docs/source/tutorials/configuration.rst | 2 +- example_scenes/basic.py | 34 +- manim/__init__.py | 1 + manim/__main__.py | 33 +- manim/_config/__init__.py | 2 +- manim/_config/cfg_subcmds.py | 4 +- manim/_config/default.cfg | 2 +- manim/_config/utils.py | 9 +- manim/animation/animation.py | 10 +- manim/animation/creation.py | 8 +- manim/animation/fading.py | 121 ++- manim/animation/indication.py | 4 +- manim/animation/movement.py | 25 +- manim/animation/numbers.py | 16 +- manim/animation/transform.py | 26 +- manim/camera/camera.py | 21 +- manim/camera/mapping_camera.py | 2 +- manim/camera/moving_camera.py | 4 +- manim/camera/multi_camera.py | 4 +- manim/camera/three_d_camera.py | 19 +- manim/grpc/gen/frameserver_pb2.py | 809 +++++++++++++++--- manim/grpc/gen/frameserver_pb2_grpc.py | 86 +- manim/grpc/gen/renderserver_pb2.py | 457 +++------- manim/grpc/gen/renderserver_pb2_grpc.py | 203 +---- manim/grpc/impl/frame_server_impl.py | 435 +++++----- manim/grpc/proto/frameserver.proto | 71 +- manim/grpc/proto/renderserver.proto | 47 +- manim/mobject/changing.py | 2 +- manim/mobject/geometry.py | 24 +- manim/mobject/graph.py | 305 +++++++ manim/mobject/matrix.py | 2 +- manim/mobject/mobject.py | 73 +- manim/mobject/mobject_update_utils.py | 4 +- manim/mobject/numbers.py | 7 +- manim/mobject/shape_matchers.py | 2 +- manim/mobject/svg/code_mobject.py | 33 +- manim/mobject/svg/svg_mobject.py | 12 +- manim/mobject/svg/tex_mobject.py | 11 +- manim/mobject/svg/text_mobject.py | 599 +++++++++---- manim/mobject/three_dimensions.py | 1 + manim/mobject/types/image_mobject.py | 24 +- manim/mobject/types/vectorized_mobject.py | 51 +- manim/mobject/value_tracker.py | 4 +- manim/renderer/cairo_renderer.py | 23 +- manim/renderer/js_renderer.py | 51 ++ manim/scene/graph_scene.py | 12 +- manim/scene/moving_camera_scene.py | 23 +- manim/scene/reconfigurable_scene.py | 2 +- manim/scene/scene.py | 424 ++++----- manim/scene/scene_file_writer.py | 17 +- manim/scene/three_d_scene.py | 40 +- manim/scene/vector_space_scene.py | 2 +- manim/scene/zoomed_scene.py | 8 +- manim/utils/bezier.py | 2 +- manim/utils/caching.py | 4 +- manim/utils/config_ops.py | 2 +- manim/utils/family.py | 7 +- manim/utils/file_ops.py | 4 +- manim/utils/hashing.py | 14 +- manim/utils/iterables.py | 2 +- manim/utils/module_ops.py | 10 + manim/utils/tex.py | 10 +- manim/utils/tex_file_writing.py | 6 +- poetry.lock | 381 ++++----- pyproject.toml | 11 +- scripts/simple_cors_http_server.py | 17 + .../update_protos.py | 12 +- tests/conftest.py | 2 +- .../geometry/ElbowTest.npz | Bin 0 -> 1912 bytes .../geometry/ZIndexTest.npz | Bin 0 -> 7501 bytes .../graphical_units_data/plot/AxesTest.npz | Bin 0 -> 4871 bytes .../plot/PlotFunctions.npz | Bin 0 -> 14719 bytes .../transform/AnimationBuilderTest.npz | Bin 0 -> 2914 bytes .../UpdateSceneDuringAnimationTest.npz | Bin 0 -> 6255 bytes tests/helpers/video_utils.py | 2 +- tests/test_config.py | 2 +- tests/test_graph.py | 11 + tests/test_graphical_units/test_axes.py | 2 +- tests/test_graphical_units/test_creation.py | 6 - tests/test_graphical_units/test_geometry.py | 17 +- .../{test_graph.py => test_graphscene.py} | 2 +- tests/test_graphical_units/test_movements.py | 4 +- tests/test_graphical_units/test_transform.py | 7 +- tests/test_graphical_units/test_updaters.py | 11 +- tests/utils/GraphicalUnitTester.py | 4 +- tests/utils/logging_tester.py | 6 +- 94 files changed, 3082 insertions(+), 1948 deletions(-) create mode 100644 .gitattributes create mode 100644 manim/mobject/graph.py create mode 100644 manim/renderer/js_renderer.py create mode 100755 scripts/simple_cors_http_server.py rename manim/grpc/update_protos.sh => scripts/update_protos.py (53%) create mode 100644 tests/control_data/graphical_units_data/geometry/ElbowTest.npz create mode 100644 tests/control_data/graphical_units_data/geometry/ZIndexTest.npz create mode 100644 tests/control_data/graphical_units_data/plot/AxesTest.npz create mode 100644 tests/control_data/graphical_units_data/plot/PlotFunctions.npz create mode 100644 tests/control_data/graphical_units_data/transform/AnimationBuilderTest.npz create mode 100644 tests/control_data/graphical_units_data/updaters/UpdateSceneDuringAnimationTest.npz create mode 100644 tests/test_graph.py rename tests/test_graphical_units/{test_graph.py => test_graphscene.py} (97%) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..67d1b87e35 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +manim/grpc/gen/* linguist-generated=true diff --git a/docker/Dockerfile b/docker/Dockerfile index d7ce166776..0d9d81c1fb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,9 +26,6 @@ COPY . /opt/manim WORKDIR /opt/manim RUN pip install --no-cache . -# ensure that ffi bindings are generated -RUN python -c "import pangocairocffi" - # create working directory for user to mount local directory into WORKDIR /manim RUN chmod 666 /manim diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 33544dbe36..e3ed90a1b9 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -17,6 +17,100 @@ Upcoming release Changes for the upcoming release are tracked `in our GitHub wiki <https://github.com/ManimCommunity/manim/wiki/Changelog-for-next-release>`_. +****** +v0.2.0 +****** + +:Date: January 1, 2021 + +The changes since Manim Community release v0.1.1 are listed below. + +Breaking Changes +================ + +- Remove all CONFIG dictionaries and all calls to ``digest_config`` and allow + passing options directly to the constructor of the corresponding classes (:pr:`783`). + + Practically, this means that old constructions using ``CONFIG`` like:: + + class SomeMobject(Thing): + CONFIG = { + "my_awesome_property": 42 + } + + where corresponding objects were then instantiated as ``my_mobject = SomeMobject()`` + should now be created simply using ``my_mobject = SomeMobject(my_awesome_property=42)``. + +- Remove old syntax for animating mobject methods by passing the methods and arguments to ``self.play``, + and use a new syntax featuring the ``animate`` property (:pr:`881`). + + For example: the old-style ``play`` call + :: + + self.play(my_square.shift, LEFT) + + should be replaced with the new following call using the ``animate`` property:: + + self.play(my_square.animate.shift(LEFT)) + +New Features +============ + +- Added creation animation for :class:`~.ManimBanner` (:pr:`814`) +- Added some documentation to :meth:`~.Scene.construct` (:pr:`753`) +- Added a black and white monochromatic version of Manim's logo (:pr:`826`) +- Added support for a plugin system (``manim plugin`` subcommand + documentation) (:pr:`784`) +- Implemented ``__add__``, ``__iadd__``, ``__sub__``, and ``__isub__`` for :class:`~.Mobject` (allowing for notation like ``some_vgroup + some_mobject``) (:pr:`790`) +- Added type hints to several files in the library (:pr:`835`) +- Added some examples to :mod:`~.animation.creation` (:pr:`820`) +- Added some examples to :class:`~.DashedLine` and :class:`~.CurvesAsSubmobjects` (:pr:`833`) +- Added new implementation for text rendered with Pango, :class:`~.MarkupText`, which can be formatted with an HTML-like syntax (:pr:`855`) +- Added Fading in and out examples and deprecation of ``FadeInFromDown`` and ``FadeOutAndShiftDown`` (:pr:`827`) +- Added example for :class:`~.MoveAlongPath` to the docs (:pr:`873`) +- Added ambient rotate for other angles - theta, phi, gamma (:pr:`660`) +- Use custom bindings for Pango (:pr:`878`) +- Added :class:`~.Graph`, a basic implementation for (graph theory) graphs (:pr:`861`) +- Allow for chaining methods when using the new ``.animate`` syntax in :meth:`~.Scene.play` (:pr:`889`) + +Bugfixes +======== + +- Fix doctests in .rst files (:pr:`797`) +- Fix failing doctest after adding ``manim plugin`` subcommand (:pr:`831`) +- Normalize the direction vector in :meth:`~.mobject_update_utils.always_shift` (:pr:`839`) +- Add ``disable_ligatures`` to :class:`~.Text` (via :pr:`804`) +- Make scene caching aware of order of Mobjects (:pr:`845`) +- Fix :class:`~.CairoText` to work with new config structure (:pr:`858`) +- Added missing argument to classes inheriting from :class:`~.Matrix` (:pr:`859`) +- Fixed: ``z_index`` of mobjects contained in others as submobjects is now properly respected (:pr:`872`) +- Let :meth:`~.ParametricSurface.set_fill_by_checkboard` return the modified surface to allow method chaining (:pr:`883`) +- Mobjects added during an updater are added to ``Scene.moving_mobjects`` (:pr:`838`) +- Pass background color to JS renderer (:pr:`876`) +- Small fixes to docstrings. Tiny cleanups. Remove ``digest_mobject_attrs``. (:pr:`834`) +- Added closed shape detection in :class:`~.DashedVMobject` in order to achieve an even dash pattern (:pr:`884`) +- Fix Spelling in docstrings and variables across the library (:pr:`890`) + +Other changes +============= + +- Change library name to manim (:pr:`811`) +- Docker: use local files when building an image (:pr:`803`) +- Let ffmpeg render partial movie files directly instead of temp files (:pr:`817`) +- ``manimce`` to ``manim`` & capitalizing Manim in readme (:pr:`794`) +- Added flowchart for different docstring categories (:pr:`828`) +- Improve example in module docstring of :mod:`~.animation.creation` + explicitly document buff parameter in :meth:`~.Mobject.arrange` (:pr:`825`) +- Disable CI pipeline for Python 3.6 (:pr:`823`) +- Update URLs in docs (:pr:`832`) +- Move upcoming changelog to GitHub-wiki (:pr:`822`) +- Change badges in readme (:pr:`854`) +- Exclude generated gRPC files from source control (:pr:`868`) +- Added linguist-generated attribute to ``.gitattributes`` (:pr:`877`) +- Cleanup: removed inheritance from ``object`` for some classes, refactor some imports (:pr:`795`) +- Change several ``str.format()`` to ``f``-strings (:pr:`867`) +- Update javascript renderer (:pr:`830`) +- Bump version number to 0.2.0, update changelog (:pr:`894`) + + ****** v0.1.1 ****** @@ -105,7 +199,7 @@ Command line #. Output of 'manim --help' has been improved #. Implement logging with the :code:`rich` library and a :code:`logger` object instead of plain ol' prints -#. Added a flag :code:`--dry_run`, which doesn’t write any media +#. Added a flag :code:`--dry_run`, which doesn't write any media #. Allow for running manim with :code:`python3 -m manim` #. Refactored Tex Template management. You can now use custom templates with command line args using :code:`--tex_template`! #. Re-add :code:`--save_frames` flag, which will save each frame as a png @@ -187,7 +281,7 @@ Of interest to developers #. Python code formatting is now enforced by using the :code:`black` tool #. PRs now require two approving code reviews from community devs before they can be merged -#. Added tests to ensure stuff doesn’t break between commits (For developers) [Uses Github CI, and Pytest] +#. Added tests to ensure stuff doesn't break between commits (For developers) [Uses Github CI, and Pytest] #. Add contribution guidelines (for developers) #. Added autogenerated documentation with sphinx and autodoc/autosummary [WIP] #. Made manim internally use relative imports @@ -203,4 +297,4 @@ Other Changes #. Cleanup 3b1b Specific Files #. Rename package from manimlib to manim #. Move all imports to :code:`__init__`, so :code:`from manim import *` replaces :code:`from manimlib.imports import *` -#. Global dir variable handling has been removed. Instead :code:`initialize_directories`, if needed, overrides the values from the cfg files at runtime. \ No newline at end of file +#. Global dir variable handling has been removed. Instead :code:`initialize_directories`, if needed, overrides the values from the cfg files at runtime. diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 1a914d08ae..aa43200bb6 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -49,18 +49,6 @@ Basic Concepts self.add(logo) -.. manim:: GradientImageFromArray - :save_last_frame: - :ref_classes: ImageMobject - - class GradientImageFromArray(Scene): - def construct(self): - n = 256 - imageArray = np.uint8( - [[i * 256 / n for i in range(0, n)] for _ in range(0, n)] - ) - image = ImageMobject(imageArray).scale(2) - self.add(image) .. manim:: BraceAnnotation :save_last_frame: @@ -91,6 +79,19 @@ Basic Concepts tip_text = Text('(2, 2)').next_to(arrow.get_end(), RIGHT) self.add(numberplane, dot, arrow, origin_text, tip_text) +.. manim:: GradientImageFromArray + :save_last_frame: + :ref_classes: ImageMobject + + class GradientImageFromArray(Scene): + def construct(self): + n = 256 + imageArray = np.uint8( + [[i * 256 / n for i in range(0, n)] for _ in range(0, n)] + ) + image = ImageMobject(imageArray).scale(2) + self.add(image) + .. manim:: BezierSpline :save_last_frame: :ref_classes: Line VGroup @@ -203,10 +204,10 @@ Animations def construct(self): square = Square(color=BLUE, fill_opacity=1) - self.play(square.shift, LEFT) - self.play(square.set_fill, ORANGE) - self.play(square.scale, 0.3) - self.play(square.rotate, 0.4) + self.play(square.animate.shift(LEFT)) + self.play(square.animate.set_fill(ORANGE)) + self.play(square.animate.scale(0.3)) + self.play(square.animate.rotate(0.4)) .. manim:: MovingFrameBox :ref_modules: manim.mobject.svg.tex_mobject @@ -267,8 +268,8 @@ Animations self.add(path, dot) self.play(Rotating(dot, radians=PI, about_point=RIGHT, run_time=2)) self.wait() - self.play(dot.shift, UP) - self.play(dot.shift, LEFT) + self.play(dot.animate.shift(UP)) + self.play(dot.animate.shift(LEFT)) self.wait() @@ -393,9 +394,9 @@ Special Camera Settings moving_dot = Dot().move_to(graph.points[0]).set_color(ORANGE) dot_at_start_graph = Dot().move_to(graph.points[0]) - dot_at_end_grap = Dot().move_to(graph.points[-1]) - self.add(graph, dot_at_end_grap, dot_at_start_graph, moving_dot) - self.play( self.camera_frame.scale,0.5,self.camera_frame.move_to,moving_dot) + dot_at_end_graph = Dot().move_to(graph.points[-1]) + self.add(graph, dot_at_end_graph, dot_at_start_graph, moving_dot) + self.play(self.camera_frame.animate.scale(0.5).move_to(moving_dot)) def update_curve(mob): mob.move_to(moving_dot.get_center()) @@ -461,15 +462,15 @@ Special Camera Settings # Scale in x y z scale_factor = [0.5, 1.5, 0] self.play( - frame.scale, scale_factor, - zoomed_display.scale, scale_factor, + frame.animate.scale(scale_factor), + zoomed_display.animate.scale(scale_factor), FadeOut(zoomed_camera_text), FadeOut(frame_text) ) self.wait() self.play(ScaleInPlace(zoomed_display, 2)) self.wait() - self.play(frame.shift, 2.5 * DOWN) + self.play(frame.animate.shift(2.5 * DOWN)) self.wait() self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera, rate_func=lambda t: smooth(1 - t)) self.play(Uncreate(zoomed_display_frame), FadeOut(frame)) @@ -603,8 +604,8 @@ Advanced Projects class OpeningManim(Scene): def construct(self): - title = Tex("This is some \\LaTeX") - basel = MathTex("\\sum_{n=1}^\\infty " "\\frac{1}{n^2} = \\frac{\\pi^2}{6}") + title = Tex(r"This is some \LaTeX") + basel = MathTex(r"\sum_{n=1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}") VGroup(title, basel).arrange(DOWN) self.play( Write(title), @@ -616,7 +617,7 @@ Advanced Projects transform_title.to_corner(UP + LEFT) self.play( Transform(title, transform_title), - LaggedStart(*map(lambda obj: FadeOutAndShift(obj, direction=DOWN), basel)), + LaggedStart(*[FadeOutAndShift(obj, direction=DOWN) for obj in basel]), ) self.wait() @@ -634,19 +635,20 @@ Advanced Projects self.wait() grid_transform_title = Tex( - "That was a non-linear function \\\\" "applied to the grid" + r"That was a non-linear function \\ applied to the grid" ) grid_transform_title.move_to(grid_title, UL) grid.prepare_for_nonlinear_transform() self.play( - grid.apply_function, - lambda p: p - + np.array( - [ - np.sin(p[1]), - np.sin(p[0]), - 0, - ] + grid.animate.apply_function( + lambda p: p + + np.array( + [ + np.sin(p[1]), + np.sin(p[0]), + 0, + ] + ) ), run_time=3, ) @@ -679,7 +681,7 @@ Advanced Projects self.add(x_axis, y_axis) self.add_x_labels() - self.orgin_point = np.array([-4,0,0]) + self.origin_point = np.array([-4,0,0]) self.curve_start = np.array([-3,0,0]) def add_x_labels(self): @@ -694,14 +696,14 @@ Advanced Projects def show_circle(self): circle = Circle(radius=1) - circle.move_to(self.orgin_point) + circle.move_to(self.origin_point) self.add(circle) self.circle = circle def move_dot_and_draw_curve(self): orbit = self.circle - orgin_point = self.orgin_point + origin_point = self.origin_point dot = Dot(radius=0.08, color=YELLOW) dot.move_to(orbit.point_from_proportion(0)) @@ -714,7 +716,7 @@ Advanced Projects mob.move_to(orbit.point_from_proportion(self.t_offset % 1)) def get_line_to_circle(): - return Line(orgin_point, dot.get_center(), color=BLUE) + return Line(origin_point, dot.get_center(), color=BLUE) def get_line_to_curve(): x = self.curve_start[0] + self.t_offset * 4 diff --git a/docs/source/installation/mac.rst b/docs/source/installation/mac.rst index d39e633066..25064a4c19 100644 --- a/docs/source/installation/mac.rst +++ b/docs/source/installation/mac.rst @@ -15,16 +15,6 @@ To install cairo: brew install cairo -To install Pango and it dependencies: - -.. code-block:: bash - - brew install pkg-config - brew install libffi - brew install pango - brew install glib - - To install ffmpeg: .. code-block:: bash diff --git a/docs/source/installation/troubleshooting.rst b/docs/source/installation/troubleshooting.rst index 21917fa23f..f679909dd6 100644 --- a/docs/source/installation/troubleshooting.rst +++ b/docs/source/installation/troubleshooting.rst @@ -3,30 +3,28 @@ Troubleshooting List of known installation problems. -(Windows) OSError: dlopen() failed to load a library: pango? ------------------------------------------------------------- - -If your manual installation of Manim (or even the installation using -Chocolatey) fails with the error +``pip install manim`` fails when installing manimpango? +------------------------------------------------------- +Most likely this means that pip was not able to use our pre-built wheels +of ``manimpango``. Let us know (via our `Discord <https://discord.gg/mMRrZQW>`_ +or by opening a +`new issue on GitHub <https://github.com/ManimCommunity/ManimPango/issues/new>`_) +which architecture you would like to see supported, and we'll see what we +can do about it. + +To fix errors when installing ``manimpango``, you need to make sure you +have all the necessary build requirements. Check out the detailed +instructions given in +`the BUILDING section <https://github.com/ManimCommunity/ManimPango#BUILDING>`_ +of the corresponding `GitHub repository <https://github.com/ManimCommunity/ManimPango>`_. -.. code-block:: - OSError: dlopen() failed to load a library: pango / pango-1 / pango-1.0 / pango-1.0-0 - -possibly combined with alerts warning about procedure entry points -``"deflateSetHeader"`` and ``"inflateReset2"`` that could not be -located, you might run into an issue with a patched version of ``zlib1.dll`` -shipped by Intel, `as described here <https://github.com/msys2/MINGW-packages/issues/813>`_. +(Windows) OSError: dlopen() failed to load a library: pango? +------------------------------------------------------------ -To resolve this issue, you can copy ``zlib1.dll`` from the directory -provided for the Pango binaries to the directory Manim is installed to. +This should be fixed in Manim's latest version, update +using ``pip install --upgrade manim``. -For a more global solution (try at your own risk!), try renaming the -file ``zlib1.dll`` located at ``C:\Program Files\Intel\Wifi\bin`` to -something like ``zlib1.dll.bak`` -- and then try installing Manim again -(either using ``pip install manim`` or with Chocolatey). Ensure that -you are able to revert this name change in case you run into troubles -with your WiFi (we did not get any reports about such a problem, however). Some letters are missing from TextMobject/TexMobject output? diff --git a/docs/source/installation/win.rst b/docs/source/installation/win.rst index f9d301d13b..30b8824d2f 100644 --- a/docs/source/installation/win.rst +++ b/docs/source/installation/win.rst @@ -18,32 +18,6 @@ You can install manim very easily using chocolatey, by typing the following comm And then you can skip all the other steps and move to installing :ref:`latex-installation`. Please see :doc:`troubleshooting` section for details about OSError. -Pango Installation -****************** - -These steps would get you `libpango-1.0-0.dll` to your ``PATH`` along -with other dependencies. You may probably have them before itself if -you have installed `GTK <https://www.gtk.org/>`_ or any ``GTK`` -based app like emacs. If you have it you can just add it to your -path and skip these steps. - -1. Go to `Release Page - <https://github.com/ManimCommunity/manim-windows/releases/latest>`_ - and download the one according to your PC architechture. - - .. important:: Please download the ``zip`` file for architechture of python installed. - It is possible to have installed ``x86`` python installed on ``x64`` PC. - -2. Extract the zip file using File Explorer or 7z to the loaction you want to install. - - .. code-block:: bash - - 7z x pango-windows-binaires-x64.zip -oC:\Pango - -3. Finally, add it `PATH variable - <https://www.computerhope.com/issues/ch000549.htm>`_. - - FFmpeg installation ******************* diff --git a/docs/source/reference.rst b/docs/source/reference.rst index d4e8d77b61..a4cfab6ecc 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -22,6 +22,7 @@ Mobjects ~mobject.frame ~mobject.functions ~mobject.geometry + ~mobject.graph ~mobject.logo ~mobject.matrix ~mobject.mobject diff --git a/docs/source/tutorials/configuration.rst b/docs/source/tutorials/configuration.rst index 44e0d97ec1..219b981e70 100644 --- a/docs/source/tutorials/configuration.rst +++ b/docs/source/tutorials/configuration.rst @@ -4,7 +4,7 @@ Configuration Manim provides an extensive configuration system that allows it to adapt to many different use cases. There are many configuration options that can be configured at different times during the scene rendering process. Each option -can be configured programatically via `the ManimConfig class`_, or at the time +can be configured programmatically via `the ManimConfig class`_, or at the time of command invocation via `command line arguments`_, or at the time the library is first imported via `the config files`_. diff --git a/example_scenes/basic.py b/example_scenes/basic.py index 8f50f8bbd5..37423b20de 100644 --- a/example_scenes/basic.py +++ b/example_scenes/basic.py @@ -9,15 +9,15 @@ # Use -s to skip to the end and just save the final frame # Use the -p to have preview of the animation (or image, if -s was # used) pop up once done. -# Use -n <number> to skip ahead to the n'th animation of a scene. +# Use -n <number> to skip ahead to the nth animation of a scene. # Use -r <number> to specify a resolution (for example, -r 1080 # for a 1920x1080 video) -class OpeningManimExample(Scene): +class OpeningManim(Scene): def construct(self): - title = Tex("This is some \\LaTeX") - basel = MathTex("\\sum_{n=1}^\\infty " "\\frac{1}{n^2} = \\frac{\\pi^2}{6}") + title = Tex(r"This is some \LaTeX") + basel = MathTex(r"\sum_{n=1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}") VGroup(title, basel).arrange(DOWN) self.play( Write(title), @@ -29,7 +29,7 @@ def construct(self): transform_title.to_corner(UP + LEFT) self.play( Transform(title, transform_title), - LaggedStart(*map(FadeOutAndShiftDown, basel)), + LaggedStart(*[FadeOutAndShift(obj, direction=DOWN) for obj in basel]), ) self.wait() @@ -41,25 +41,26 @@ def construct(self): self.add(grid, grid_title) # Make sure title is on top of grid self.play( FadeOut(title), - FadeInFromDown(grid_title), + FadeInFrom(grid_title, direction=DOWN), ShowCreation(grid, run_time=3, lag_ratio=0.1), ) self.wait() grid_transform_title = Tex( - "That was a non-linear function \\\\" "applied to the grid" + r"That was a non-linear function \\ applied to the grid" ) grid_transform_title.move_to(grid_title, UL) grid.prepare_for_nonlinear_transform() self.play( - grid.apply_function, - lambda p: p - + np.array( - [ - np.sin(p[1]), - np.sin(p[0]), - 0, - ] + grid.animate.apply_function( + lambda p: p + + np.array( + [ + np.sin(p[1]), + np.sin(p[0]), + 0, + ] + ) ), run_time=3, ) @@ -121,8 +122,7 @@ def construct(self): decimal.add_updater(lambda d: d.set_value(square.get_center()[1])) self.add(square, decimal) self.play( - square.to_edge, - DOWN, + square.animate.to_edge(DOWN), rate_func=there_and_back, run_time=5, ) diff --git a/manim/__init__.py b/manim/__init__.py index fd34b97dbd..727a5aa414 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -33,6 +33,7 @@ from .mobject.frame import * from .mobject.functions import * from .mobject.geometry import * +from .mobject.graph import * from .mobject.logo import * from .mobject.matrix import * from .mobject.mobject import * diff --git a/manim/__main__.py b/manim/__main__.py index 55858f0333..642720066e 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -7,6 +7,7 @@ get_module, get_scene_classes_from_module, get_scenes_to_render, + scene_classes_from_file, ) from manim.utils.file_ops import open_file as open_media_file from manim._config.main_utils import parse_args @@ -75,26 +76,28 @@ def main(): else: config.digest_args(args) - - module = get_module(config.get_dir("input_file")) - all_scene_classes = get_scene_classes_from_module(module) - scene_classes_to_render = get_scenes_to_render(all_scene_classes) - for SceneClass in scene_classes_to_render: + input_file = config.get_dir("input_file") + if config["use_js_renderer"]: try: - if config["use_js_renderer"]: - if frame_server_impl is None: - raise ImportError( - "Dependencies for JS renderer is not installed." - ) - frame_server_impl.get(SceneClass).start() - else: - scene = SceneClass() - scene.render() - open_file_if_needed(scene.renderer.file_writer) + if frame_server_impl is None: + raise ImportError("Dependencies for JS renderer is not installed.") + server = frame_server_impl.get(input_file) + server.start() + server.wait_for_termination() except Exception: print("\n\n") traceback.print_exc() print("\n\n") + else: + for SceneClass in scene_classes_from_file(input_file): + try: + scene = SceneClass() + scene.render() + open_file_if_needed(scene.renderer.file_writer) + except Exception: + print("\n\n") + traceback.print_exc() + print("\n\n") if __name__ == "__main__": diff --git a/manim/_config/__init__.py b/manim/_config/__init__.py index 55c54c8321..9f08566b16 100644 --- a/manim/_config/__init__.py +++ b/manim/_config/__init__.py @@ -65,7 +65,7 @@ def tempconfig(temp: Union[ManimConfig, dict]) -> _GeneratorContextManager: temp = {k: v for k, v in temp.items() if k in original} - # In order to change the config that every module has acces to, use + # In order to change the config that every module has access to, use # update(), DO NOT use assignment. Assigning config = some_dict will just # make the local variable named config point to a new dictionary, it will # NOT change the dictionary that every module has a reference to. diff --git a/manim/_config/cfg_subcmds.py b/manim/_config/cfg_subcmds.py index f9e88245a4..fe7e6fac2f 100644 --- a/manim/_config/cfg_subcmds.py +++ b/manim/_config/cfg_subcmds.py @@ -90,7 +90,7 @@ def is_valid_style(style: str) -> bool: def replace_keys(default: dict) -> dict: - """Replaces _ to . and viceversa in a dictionary for rich + """Replaces _ to . and vice versa in a dictionary for rich Parameters ---------- default : :class:`dict` @@ -98,7 +98,7 @@ def replace_keys(default: dict) -> dict: Returns ------- :class:`dict` - The dictionary which is modified by replcaing _ with . and viceversa + The dictionary which is modified by replacing _ with . and vice versa """ for key in default: if "_" in key: diff --git a/manim/_config/default.cfg b/manim/_config/default.cfg index 7647738f76..822bbea35f 100644 --- a/manim/_config/default.cfg +++ b/manim/_config/default.cfg @@ -95,7 +95,7 @@ js_renderer_path = png_mode = RGB movie_file_extension = .mp4 -# These can be overriden with any of -l (--low_quality), -m +# These can be overridden with any of -l (--low_quality), -m # (--medium_quality), -e (--high_quality), or -k (--fourk_quality). The # overriding values are found in the corresponding sections. frame_rate = 60 diff --git a/manim/_config/utils.py b/manim/_config/utils.py index b47054e78c..a6ae435335 100644 --- a/manim/_config/utils.py +++ b/manim/_config/utils.py @@ -392,7 +392,7 @@ def __deepcopy__(self, memo: typing.Dict[str, typing.Any]) -> "ManimConfig": c = ManimConfig() # Deepcopying the underlying dict is enough because all properties # either read directly from it or compute their value on the fly from - # vaulues read directly from it. + # values read directly from it. c._d = copy.deepcopy(self._d, memo) return c @@ -450,7 +450,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig": ---------- parser : :class:`ConfigParser` An object reflecting the contents of one or many ``.cfg`` files. In - particular, it may reflect the contents of mulitple files that have + particular, it may reflect the contents of multiple files that have been parsed in a cascading fashion. Returns @@ -612,6 +612,7 @@ def digest_args(self, args: argparse.Namespace) -> "ManimConfig": "scene_names", "verbosity", "background_color", + "use_js_renderer", ]: if hasattr(args, key): attr = getattr(args, key) @@ -1011,14 +1012,14 @@ def dry_run(self, val: bool) -> None: raise ValueError( "It is unclear what it means to set dry_run to " "False. Instead, try setting each option " - "individually. (write_to_movie, write_alll, " + "individually. (write_to_movie, write_all, " "save_last_frame, save_pngs, or save_as_gif)" ) @property def use_js_renderer(self): """Whether to use JS renderer or not (default).""" - self._d["use_js_renderer"] + return self._d["use_js_renderer"] @use_js_renderer.setter def use_js_renderer(self, val: bool) -> None: diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 214451584a..919dd5cc6e 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -99,7 +99,7 @@ def create_starting_mobject(self) -> Mobject: def get_all_mobjects(self) -> typing.Tuple[Mobject, typing.Union[Mobject, None]]: """ - Ordering must match the ording of arguments to interpolate_submobject + Ordering must match the ordering of arguments to interpolate_submobject """ return self.mobject, self.starting_mobject @@ -128,10 +128,6 @@ def get_all_mobjects_to_update(self) -> list: def copy(self) -> "Animation": return deepcopy(self) - def update_config(self, **kwargs: typing.Dict[str, typing.Any]) -> "Animation": - self.__dict__.update(kwargs) - return self - # Methods for interpolation, the mean of an Animation def interpolate(self, alpha: float) -> None: alpha = np.clip(alpha, 0, 1) @@ -155,13 +151,13 @@ def interpolate_mobject(self, alpha: float) -> None: self.interpolate_submobject(*mobs, sub_alpha) def interpolate_submobject( - self, submobject: Mobject, starting_sumobject: Mobject, alpha: float + self, submobject: Mobject, starting_submobject: Mobject, alpha: float ) -> None: # Typically implemented by subclass pass def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int): - # TODO, make this more understanable, and/or combine + # TODO, make this more understandable, and/or combine # its functionality with AnimationGroup's method # build_animations_with_timings lag_ratio = self.lag_ratio diff --git a/manim/animation/creation.py b/manim/animation/creation.py index 1af6f90601..5b601a10fe 100644 --- a/manim/animation/creation.py +++ b/manim/animation/creation.py @@ -106,10 +106,10 @@ def __init__(self, mobject: VMobject, **kwargs): super().__init__(mobject, **kwargs) def interpolate_submobject( - self, submobject: Mobject, starting_sumobject: Mobject, alpha: float + self, submobject: Mobject, starting_submobject: Mobject, alpha: float ) -> None: submobject.pointwise_become_partial( - starting_sumobject, *self._get_bounds(alpha) + starting_submobject, *self._get_bounds(alpha) ) def _get_bounds(self, alpha: float) -> None: @@ -236,14 +236,14 @@ def get_all_mobjects(self) -> typing.List[typing.Union[Mobject, None]]: return [*super().get_all_mobjects(), self.outline] def interpolate_submobject( - self, submobject: Mobject, starting_sumobject: Mobject, outline, alpha: float + self, submobject: Mobject, starting_submobject: Mobject, outline, alpha: float ) -> None: # Fixme: not matching the parent class? What is outline doing here? index, subalpha = integer_interpolate(0, 2, alpha) if index == 0: submobject.pointwise_become_partial(outline, 0, subalpha) submobject.match_style(outline) else: - submobject.interpolate(outline, starting_sumobject, subalpha) + submobject.interpolate(outline, starting_submobject, subalpha) class Write(DrawBorderThenFill): diff --git a/manim/animation/fading.py b/manim/animation/fading.py index 0871206dce..4a9267641c 100644 --- a/manim/animation/fading.py +++ b/manim/animation/fading.py @@ -1,13 +1,63 @@ -"""Fading in and out of view.""" +"""Fading in and out of view. + +.. manim:: Example + :hide_source: + + class Example(Scene): + def construct(self): + s1 = Square().set_color(BLUE) + s2 = Square().set_color(BLUE) + s3 = Square().set_color(BLUE) + s4 = Square().set_color(BLUE) + s5 = Square().set_color(BLUE) + s6 = Square().set_color(RED) + s7 = Square().set_color(RED) + s8 = Square().set_color(RED) + VGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.9).shift(UP) + VGroup(s5, s6, s7, s8).set_x(0).arrange(buff=1.9).shift(2 * DOWN) + t1 = Text("FadeIn").scale(0.5).next_to(s1, UP) + t2 = Text("FadeInFromPoint").scale(0.5).next_to(s2, UP) + t3 = Text("FadeInFrom").scale(0.5).next_to(s3, UP) + t4 = Text("VFadeIn").scale(0.5).next_to(s4, UP) + t5 = Text("FadeInFromLarge").scale(0.4).next_to(s5, UP) + t6 = Text("FadeOut").scale(0.45).next_to(s6, UP) + t7 = Text("FadeOutAndShift").scale(0.45).next_to(s7, UP) + t8 = Text("VFadeOut").scale(0.45).next_to(s8, UP) + + objs = [ManimBanner().scale(0.25) for _ in range(1, 9)] + objs[0].move_to(s1.get_center()) + objs[1].move_to(s2.get_center()) + objs[2].move_to(s3.get_center()) + objs[3].move_to(s4.get_center()) + objs[4].move_to(s5.get_center()) + objs[5].move_to(s6.get_center()) + objs[6].move_to(s7.get_center()) + objs[7].move_to(s8.get_center()) + self.add(s1, s2, s3, s4, s5, s6, s7, s8, t1, t2, t3, t4, t5, t6, t7, t8) + self.add(*objs) + + self.play( + FadeIn(objs[0]), + FadeInFromPoint(objs[1], s2.get_center()), + FadeInFrom(objs[2], LEFT*0.2), + VFadeIn(objs[3]), + FadeInFromLarge(objs[4]), + ) + self.wait(0.3) + self.play( + FadeOut(objs[5]), + FadeOutAndShift(objs[6], DOWN), + VFadeOut(objs[7]) + ) + +""" __all__ = [ "FadeOut", "FadeIn", "FadeInFrom", - "FadeInFromDown", "FadeOutAndShift", - "FadeOutAndShiftDown", "FadeInFromPoint", "FadeInFromLarge", "VFadeIn", @@ -35,30 +85,7 @@ class FadeOut(Transform): - """A transform fading out the given mobject. - - Examples - -------- - - .. manim:: PlaneFadeOut - - class PlaneFadeOut(Scene): - def construct(self): - sq1 = Square() - sq2 = Square() - sq3 = Square() - sq1.next_to(sq2, LEFT) - sq3.next_to(sq2, RIGHT) - circ = Circle() - circ.next_to(sq2, DOWN) - - self.add(sq1, sq2, sq3, circ) - self.wait() - - self.play(FadeOut(sq1), FadeOut(sq2), FadeOut(sq3)) - self.wait() - - """ + """A transform fading out the given mobject.""" def __init__( self, @@ -111,21 +138,6 @@ def begin(self) -> None: self.starting_mobject.fade(1) -class FadeInFromDown(FadeInFrom): - """ - Identical to FadeInFrom, just with a name that - communicates the default - """ - - def __init__( - self, mobject: "Mobject", direction: np.ndarray = DOWN, **kwargs - ) -> None: - super().__init__(mobject, direction=direction, **kwargs) - logger.warning( - "FadeInFromDown is deprecated and will eventually disappear. Please use FadeInFrom(<mobject>, direction=DOWN, <other_args>) instead." - ) - - class FadeOutAndShift(FadeOut): def __init__( self, mobject: "Mobject", direction: np.ndarray = DOWN, **kwargs @@ -139,21 +151,6 @@ def create_target(self) -> "Mobject": return target -class FadeOutAndShiftDown(FadeOutAndShift): - """ - Identical to FadeOutAndShift, just with a name that - communicates the default - """ - - def __init__( - self, mobject: "Mobject", direction: np.ndarray = DOWN, **kwargs - ) -> None: - super().__init__(mobject, direction=direction, **kwargs) - logger.warning( - "FadeOutAndShiftDown is deprecated and will eventually disappear. Please use FadeOutAndShift(<mobject>, direction=DOWN, <other_args>) instead." - ) - - class FadeInFromPoint(FadeIn): def __init__( self, mobject: "Mobject", point: typing.Union["Mobject", np.ndarray], **kwargs @@ -192,13 +189,13 @@ def __init__( ) def interpolate_submobject( - self, submobject: "Mobject", starting_sumobject: "Mobject", alpha: float + self, submobject: "Mobject", starting_submobject: "Mobject", alpha: float ) -> None: submobject.set_stroke( - opacity=interpolate(0, starting_sumobject.get_stroke_opacity(), alpha) + opacity=interpolate(0, starting_submobject.get_stroke_opacity(), alpha) ) submobject.set_fill( - opacity=interpolate(0, starting_sumobject.get_fill_opacity(), alpha) + opacity=interpolate(0, starting_submobject.get_fill_opacity(), alpha) ) @@ -207,9 +204,9 @@ def __init__(self, mobject: "Mobject", remover: bool = True, **kwargs) -> None: super().__init__(mobject, remover=remover, **kwargs) def interpolate_submobject( - self, submobject: "Mobject", starting_sumobject: "Mobject", alpha: float + self, submobject: "Mobject", starting_submobject: "Mobject", alpha: float ) -> None: - super().interpolate_submobject(submobject, starting_sumobject, 1 - alpha) + super().interpolate_submobject(submobject, starting_submobject, 1 - alpha) class VFadeInThenOut(VFadeIn): diff --git a/manim/animation/indication.py b/manim/animation/indication.py index 9cc473980d..1434a1b071 100644 --- a/manim/animation/indication.py +++ b/manim/animation/indication.py @@ -330,9 +330,9 @@ def get_rotate_about_point(self) -> np.ndarray: return self.mobject.get_center() def interpolate_submobject( - self, submobject: "Mobject", starting_sumobject: "Mobject", alpha: float + self, submobject: "Mobject", starting_submobject: "Mobject", alpha: float ) -> None: - submobject.points[:, :] = starting_sumobject.points + submobject.points[:, :] = starting_submobject.points submobject.scale( interpolate(1, self.scale_value, there_and_back(alpha)), about_point=self.get_scale_about_point(), diff --git a/manim/animation/movement.py b/manim/animation/movement.py index 76581928ca..1ed440ecf5 100644 --- a/manim/animation/movement.py +++ b/manim/animation/movement.py @@ -45,9 +45,9 @@ def function_at_time_t(self, t: float) -> typing.Tuple[float, float, float]: return lambda p: self.homotopy(*p, t) def interpolate_submobject( - self, submobject: "Mobject", starting_sumobject: "Mobject", alpha: float + self, submobject: "Mobject", starting_submobject: "Mobject", alpha: float ) -> None: - submobject.points = starting_sumobject.points + submobject.points = starting_submobject.points submobject.apply_function( self.function_at_time_t(alpha), **self.apply_function_kwargs ) @@ -55,9 +55,9 @@ def interpolate_submobject( class SmoothedVectorizedHomotopy(Homotopy): def interpolate_submobject( - self, submobject: "Mobject", starting_sumobject: "Mobject", alpha: float + self, submobject: "Mobject", starting_submobject: "Mobject", alpha: float ) -> None: - Homotopy.interpolate_submobject(self, submobject, starting_sumobject, alpha) + Homotopy.interpolate_submobject(self, submobject, starting_submobject, alpha) submobject.make_smooth() @@ -69,7 +69,7 @@ def __init__( **kwargs ) -> None: """ - Complex Hootopy a function Cx[0, 1] to C + Complex Homotopy a function Cx[0, 1] to C """ def homotopy( @@ -110,6 +110,21 @@ def interpolate_mobject(self, alpha: float) -> None: class MoveAlongPath(Animation): + """Make one mobject move along the path of another mobject. + Example + -------- + .. manim:: MoveAlongPathExample + + class MoveAlongPathExample(Scene): + def construct(self): + d1 = Dot().set_color(ORANGE) + l1 = Line(LEFT, RIGHT) + l2 = VMobject() + self.add(d1, l1, l2) + l2.add_updater(lambda x: x.become(Line(LEFT, d1.get_center()).set_color(ORANGE))) + self.play(MoveAlongPath(d1, l1), rate_func=linear) + """ + def __init__( self, mobject: "Mobject", diff --git a/manim/animation/numbers.py b/manim/animation/numbers.py index b0285d48a3..4482c8c837 100644 --- a/manim/animation/numbers.py +++ b/manim/animation/numbers.py @@ -17,10 +17,10 @@ def __init__( decimal_mob: DecimalNumber, number_update_func: typing.Callable[[float], float], suspend_mobject_updating: typing.Optional[bool] = False, - **kwargs + **kwargs, ) -> None: self.check_validity_of_input(decimal_mob) - self.yell_about_depricated_configuration(**kwargs) + self.yell_about_deprecated_configuration(**kwargs) self.number_update_func = number_update_func super().__init__( decimal_mob, suspend_mobject_updating=suspend_mobject_updating, **kwargs @@ -30,20 +30,18 @@ def check_validity_of_input(self, decimal_mob: DecimalNumber) -> None: if not isinstance(decimal_mob, DecimalNumber): raise TypeError("ChangingDecimal can only take in a DecimalNumber") - def yell_about_depricated_configuration(self, **kwargs) -> None: + def yell_about_deprecated_configuration(self, **kwargs) -> None: # Obviously this would optimally be removed at # some point. for attr in ["tracked_mobject", "position_update_func"]: if attr in kwargs: warnings.warn( - """ - Don't use {} for ChangingDecimal, - that functionality has been depricated + f""" + Don't use {attr} for ChangingDecimal, + that functionality has been deprecated and you should use a mobject updater instead - """.format( - attr - ) + """ ) def interpolate_mobject(self, alpha: float) -> None: diff --git a/manim/animation/transform.py b/manim/animation/transform.py index 263d4a0ce0..8e37cb8912 100644 --- a/manim/animation/transform.py +++ b/manim/animation/transform.py @@ -47,7 +47,7 @@ def __init__( path_arc: float = 0, path_arc_axis: np.ndarray = OUT, replace_mobject_with_target_in_scene: bool = False, - **kwargs + **kwargs, ) -> None: self.path_arc = path_arc self.path_func = path_func @@ -87,8 +87,9 @@ def create_target(self) -> typing.Union[Mobject, None]: def check_target_mobject_validity(self) -> None: if self.target_mobject is None: - message = "{}.create_target not properly implemented" - raise NotImplementedError(message.format(self.__class__.__name__)) + raise NotImplementedError( + f"{self.__class__.__name__}.create_target not properly implemented" + ) def clean_up_from_scene(self, scene: "Scene") -> None: super().clean_up_from_scene(scene) @@ -126,11 +127,11 @@ def get_all_families_zipped(self) -> typing.Iterable[tuple]: # more precise typ def interpolate_submobject( self, submobject: Mobject, - starting_sumobject: Mobject, + starting_submobject: Mobject, target_copy: Mobject, alpha: float, ) -> "Transform": # doesn't match the parent class? - submobject.interpolate(starting_sumobject, target_copy, alpha, self.path_func) + submobject.interpolate(starting_submobject, target_copy, alpha, self.path_func) return self @@ -159,7 +160,7 @@ def __init__( mobject: Mobject, target_mobject: Mobject, path_arc: float = -np.pi, - **kwargs + **kwargs, ) -> None: super().__init__(mobject, target_mobject, path_arc=path_arc, **kwargs) @@ -170,7 +171,7 @@ def __init__( mobject: Mobject, target_mobject: Mobject, path_arc: float = np.pi, - **kwargs + **kwargs, ) -> None: super().__init__(mobject, target_mobject, path_arc=path_arc, **kwargs) @@ -187,6 +188,11 @@ def check_validity_of_input(self, mobject: Mobject) -> None: ) +class _MethodAnimation(MoveToTarget): + def __init__(self, mobject): + super().__init__(mobject) + + class ApplyMethod(Transform): def __init__( self, method: types.MethodType, *args, **kwargs @@ -252,7 +258,7 @@ def __init__( function: types.MethodType, mobject: Mobject, run_time: float = DEFAULT_POINTWISE_FUNCTION_RUN_TIME, - **kwargs + **kwargs, ) -> None: super().__init__(mobject.apply_function, function, run_time=run_time, **kwargs) @@ -355,14 +361,14 @@ class Swap(CyclicReplace): pass # Renaming, more understandable for two entries -# TODO, this may be depricated...worth reimplementing? +# TODO, this may be deprecated...worth reimplementing? class TransformAnimations(Transform): def __init__( self, start_anim: Animation, end_anim: Animation, rate_func: typing.Callable = squish_rate_func(smooth), - **kwargs + **kwargs, ) -> None: self.start_anim = start_anim self.end_anim = end_anim diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 146fef1268..c054bcc796 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -29,7 +29,7 @@ from ..utils.family import extract_mobject_family_members -class Camera(object): +class Camera: """Base camera class. This is the object which takes care of what exactly is displayed @@ -103,9 +103,6 @@ def __init__( frame_rate = config["frame_rate"] self.frame_rate = frame_rate - for attr in ["background_color", "background_opacity"]: - setattr(self, f"_{attr}", kwargs.get(attr, config[attr])) - for attr in ["background_color", "background_opacity"]: setattr(self, f"_{attr}", kwargs.get(attr, config[attr])) @@ -194,7 +191,7 @@ def type_or_raise(self, mobject): def reset_pixel_shape(self, new_height, new_width): """This method resets the height and width - of a single pixel to the passed new_heigh and new_width. + of a single pixel to the passed new_height and new_width. Parameters ---------- @@ -345,7 +342,7 @@ def make_background_from_func(self, coords_to_colors_func): Parameters ---------- coords_to_colors_func : function - The function whose input is an (x,y) pair of coordinats and + The function whose input is an (x,y) pair of coordinates and whose return values must be the colors for that point Returns ------- @@ -375,7 +372,7 @@ def set_background_from_func(self, coords_to_colors_func): Parameters ---------- coords_to_colors_func : function - The function whose input is an (x,y) pair of coordinats and + The function whose input is an (x,y) pair of coordinates and whose return values must be the colors for that point """ self.set_background(self.make_background_from_func(coords_to_colors_func)) @@ -882,7 +879,7 @@ def display_point_cloud(self, pmobject, points, rgbas, thickness, pixel_array): pixel_array[:, :] = new_pa.reshape((ph, pw, rgba_len)) def display_multiple_image_mobjects(self, image_mobjects, pixel_array): - """Displays multiple image mobjects by modifiying the passed pixel_array. + """Displays multiple image mobjects by modifying the passed pixel_array. Parameters ---------- @@ -929,7 +926,7 @@ def display_image_mobject(self, image_mobject, pixel_array): # TODO, there is no accounting for a shear... - # Paste into an image as large as the camear's pixel array + # Paste into an image as large as the camera's pixel array full_image = Image.fromarray( np.zeros((self.pixel_height, self.pixel_width)), mode="RGBA" ) @@ -1006,7 +1003,7 @@ def transform_points_pre_display( # NOTE: There seems to be an unused argument `mobject`. # Subclasses (like ThreeDCamera) may want to - # adjust points futher before they're shown + # adjust points further before they're shown if not np.all(np.isfinite(points)): # TODO, print some kind of warning about # mobject having invalid points? @@ -1151,7 +1148,7 @@ def get_coords_of_all_pixels(self): # NOTE: The methods of the following class have not been mentioned outside of their definitons. # Their DocStrings are not as detailed as preferred. -class BackgroundColoredVMobjectDisplayer(object): +class BackgroundColoredVMobjectDisplayer: def __init__(self, camera): """ Parameters @@ -1170,7 +1167,7 @@ def reset_pixel_array(self): def resize_background_array( self, background_array, new_width, new_height, mode="RGBA" ): - """Resizes the pixel array represinting the background. + """Resizes the pixel array representing the background. Parameters ---------- diff --git a/manim/camera/mapping_camera.py b/manim/camera/mapping_camera.py index 861b08618b..4f014d1940 100644 --- a/manim/camera/mapping_camera.py +++ b/manim/camera/mapping_camera.py @@ -117,7 +117,7 @@ def init_background(self): # A OldMultiCamera which, when called with two full-size cameras, initializes itself -# as a splitscreen, also taking care to resize each individual camera within it +# as a split screen, also taking care to resize each individual camera within it class SplitScreenCamera(OldMultiCamera): diff --git a/manim/camera/moving_camera.py b/manim/camera/moving_camera.py index 767e65f571..083b340f9a 100644 --- a/manim/camera/moving_camera.py +++ b/manim/camera/moving_camera.py @@ -45,7 +45,7 @@ def __init__( ): """ frame is a Mobject, (should almost certainly be a rectangle) - determining which region of space the camera displys + determining which region of space the camera displays """ self.fixed_dimension = fixed_dimension self.default_frame_stroke_color = default_frame_stroke_color @@ -165,7 +165,7 @@ def cache_cairo_context(self, pixel_array, ctx): def get_mobjects_indicating_movement(self): """ - Returns all mobjets whose movement implies that the camera + Returns all mobjects whose movement implies that the camera should think of all other mobjects on the screen as moving Returns diff --git a/manim/camera/multi_camera.py b/manim/camera/multi_camera.py index e4816a4760..459c2e1033 100644 --- a/manim/camera/multi_camera.py +++ b/manim/camera/multi_camera.py @@ -16,7 +16,7 @@ def __init__( allow_cameras_to_capture_their_own_display=False, **kwargs ): - """Initalises the MultiCamera + """Initialises the MultiCamera Parameters ---------- @@ -85,7 +85,7 @@ def capture_mobjects(self, mobjects, **kwargs): MovingCamera.capture_mobjects(self, mobjects, **kwargs) def get_mobjects_indicating_movement(self): - """Returns all mobjets whose movement implies that the camera + """Returns all mobjects whose movement implies that the camera should think of all other mobjects on the screen as moving Returns diff --git a/manim/camera/three_d_camera.py b/manim/camera/three_d_camera.py index 5cba4f9f26..f1316f1a28 100644 --- a/manim/camera/three_d_camera.py +++ b/manim/camera/three_d_camera.py @@ -8,16 +8,17 @@ from .. import config from ..camera.camera import Camera from ..constants import * -from ..mobject.three_d_utils import get_3d_vmob_end_corner -from ..mobject.three_d_utils import get_3d_vmob_end_corner_unit_normal -from ..mobject.three_d_utils import get_3d_vmob_start_corner -from ..mobject.three_d_utils import get_3d_vmob_start_corner_unit_normal +from ..mobject.three_d_utils import ( + get_3d_vmob_end_corner, + get_3d_vmob_end_corner_unit_normal, + get_3d_vmob_start_corner, + get_3d_vmob_start_corner_unit_normal, +) from ..mobject.types.point_cloud_mobject import Point from ..mobject.value_tracker import ValueTracker from ..utils.color import get_shaded_rgb -from ..utils.space_ops import rotation_about_z -from ..utils.space_ops import rotation_matrix from ..utils.family import extract_mobject_family_members +from ..utils.space_ops import rotation_about_z, rotation_matrix class ThreeDCamera(Camera): @@ -277,7 +278,7 @@ def project_points(self, points): zs = points[:, 2] for i in 0, 1: if self.exponential_projection: - # Proper projedtion would involve multiplying + # Proper projection would involve multiplying # x and y by d / (d-z). But for points with high # z value that causes weird artifacts, and applying # the exponential helps smooth it out. @@ -342,10 +343,10 @@ def add_fixed_orientation_mobjects( center as centerpoint, by default False center_func : func, optional The function which returns the centerpoint - with respect to which the mobjec will be oriented, by default None + with respect to which the mobject will be oriented, by default None """ # This prevents the computation of mobject.get_center - # every single time a projetion happens + # every single time a projection happens def get_static_center_func(mobject): point = mobject.get_center() return lambda: point diff --git a/manim/grpc/gen/frameserver_pb2.py b/manim/grpc/gen/frameserver_pb2.py index 288786c2b9..a1188685c3 100644 --- a/manim/grpc/gen/frameserver_pb2.py +++ b/manim/grpc/gen/frameserver_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: frameserver.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -17,27 +17,141 @@ package="frameserver", syntax="proto3", serialized_options=None, - serialized_pb=b'\n\x11\x66rameserver.proto\x12\x0b\x66rameserver"A\n\x0c\x46rameRequest\x12\x17\n\x0f\x61nimation_index\x18\x01 \x01(\x03\x12\x18\n\x10\x61nimation_offset\x18\x02 \x01(\x02"u\n\x05Style\x12\x12\n\nfill_color\x18\x01 \x01(\t\x12\x14\n\x0c\x66ill_opacity\x18\x02 \x01(\x02\x12\x14\n\x0cstroke_color\x18\x03 \x01(\t\x12\x16\n\x0estroke_opacity\x18\x04 \x01(\x02\x12\x14\n\x0cstroke_width\x18\x05 \x01(\x02"(\n\x05Point\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02"v\n\x0bMobjectData\x12\n\n\x02id\x18\x01 \x01(\x03\x12"\n\x06points\x18\x02 \x03(\x0b\x32\x12.frameserver.Point\x12!\n\x05style\x18\x03 \x01(\x0b\x32\x12.frameserver.Style\x12\x14\n\x0cneeds_redraw\x18\x04 \x01(\x08"\xb0\x01\n\rFrameResponse\x12*\n\x08mobjects\x18\x01 \x03(\x0b\x32\x18.frameserver.MobjectData\x12\x15\n\rframe_pending\x18\x02 \x01(\x08\x12\x1a\n\x12\x61nimation_finished\x18\x03 \x01(\x08\x12\x16\n\x0escene_finished\x18\x04 \x01(\x08\x12\x10\n\x08\x64uration\x18\x05 \x01(\x02\x12\x16\n\x0e\x61nimation_name\x18\x06 \x01(\t"\x17\n\x15RendererStatusRequest",\n\x16RendererStatusResponse\x12\x12\n\nscene_name\x18\x01 \x01(\t"\x16\n\x14SceneLocationRequest"\x17\n\x15SceneLocationResponse2\x8f\x02\n\x0b\x46rameServer\x12G\n\x0eGetFrameAtTime\x12\x19.frameserver.FrameRequest\x1a\x1a.frameserver.FrameResponse\x12Y\n\x0eRendererStatus\x12".frameserver.RendererStatusRequest\x1a#.frameserver.RendererStatusResponse\x12\\\n\x13UpdateSceneLocation\x12!.frameserver.SceneLocationRequest\x1a".frameserver.SceneLocationResponseb\x06proto3', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x11\x66rameserver.proto\x12\x0b\x66rameserver";\n\x16\x46\x65tchSceneDataResponse\x12!\n\x05scene\x18\x01 \x01(\x0b\x32\x12.frameserver.Scene"[\n\x05Scene\x12\x0c\n\x04name\x18\x01 \x01(\t\x12*\n\nanimations\x18\x02 \x03(\x0b\x32\x16.frameserver.Animation\x12\x18\n\x10\x62\x61\x63kground_color\x18\x03 \x01(\t"+\n\tAnimation\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08\x64uration\x18\x02 \x01(\x02"\xd5\x01\n\x0c\x46rameRequest\x12\x13\n\x0btime_offset\x18\x01 \x01(\x02\x12\x13\n\x0bstart_index\x18\x02 \x01(\x05\x12\x11\n\tend_index\x18\x03 \x01(\x05\x12\x13\n\x0bimage_index\x18\x04 \x01(\x05\x12;\n\x0cpreview_mode\x18\x05 \x01(\x0e\x32%.frameserver.FrameRequest.PreviewMode"6\n\x0bPreviewMode\x12\x07\n\x03\x41LL\x10\x00\x12\x13\n\x0f\x41NIMATION_RANGE\x10\x01\x12\t\n\x05IMAGE\x10\x02"u\n\x05Style\x12\x12\n\nfill_color\x18\x01 \x01(\t\x12\x14\n\x0c\x66ill_opacity\x18\x02 \x01(\x02\x12\x14\n\x0cstroke_color\x18\x03 \x01(\t\x12\x16\n\x0estroke_opacity\x18\x04 \x01(\x02\x12\x14\n\x0cstroke_width\x18\x05 \x01(\x02"(\n\x05Point\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02"\x97\x02\n\x0bMobjectData\x12\n\n\x02id\x18\x01 \x01(\x03\x12!\n\x05style\x18\x02 \x01(\x0b\x32\x12.frameserver.Style\x12\x32\n\x04type\x18\x03 \x01(\x0e\x32$.frameserver.MobjectData.MobjectType\x12:\n\x17vectorized_mobject_data\x18\x04 \x01(\x0b\x32\x19.frameserver.VMobjectData\x12\x39\n\x12image_mobject_data\x18\x05 \x01(\x0b\x32\x1d.frameserver.ImageMobjectData".\n\x0bMobjectType\x12\x0c\n\x08VMOBJECT\x10\x00\x12\x11\n\rIMAGE_MOBJECT\x10\x01"H\n\x0cVMobjectData\x12"\n\x06points\x18\x01 \x03(\x0b\x32\x12.frameserver.Point\x12\x14\n\x0cneeds_redraw\x18\x02 \x01(\x08"c\n\x10ImageMobjectData\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0e\n\x06height\x18\x02 \x01(\x02\x12\r\n\x05width\x18\x03 \x01(\x02\x12"\n\x06\x63\x65nter\x18\x04 \x01(\x0b\x32\x12.frameserver.Point"\xdf\x01\n\rFrameResponse\x12*\n\x08mobjects\x18\x01 \x03(\x0b\x32\x18.frameserver.MobjectData\x12\x15\n\rframe_pending\x18\x02 \x01(\x08\x12\x1a\n\x12\x61nimation_finished\x18\x03 \x01(\x08\x12\x16\n\x0escene_finished\x18\x04 \x01(\x08\x12\x10\n\x08\x64uration\x18\x05 \x01(\x02\x12\x12\n\nanimations\x18\x06 \x03(\t\x12\x17\n\x0f\x61nimation_index\x18\x07 \x01(\x05\x12\x18\n\x10\x61nimation_offset\x18\x08 \x01(\x02"\x0e\n\x0c\x45mptyRequest"\x0f\n\rEmptyResponse2\xa8\x01\n\x0b\x46rameServer\x12G\n\x0eGetFrameAtTime\x12\x19.frameserver.FrameRequest\x1a\x1a.frameserver.FrameResponse\x12P\n\x0e\x46\x65tchSceneData\x12\x19.frameserver.EmptyRequest\x1a#.frameserver.FetchSceneDataResponseb\x06proto3', ) -_FRAMEREQUEST = _descriptor.Descriptor( - name="FrameRequest", - full_name="frameserver.FrameRequest", +_FRAMEREQUEST_PREVIEWMODE = _descriptor.EnumDescriptor( + name="PreviewMode", + full_name="frameserver.FrameRequest.PreviewMode", + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name="ALL", + index=0, + number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key, + ), + _descriptor.EnumValueDescriptor( + name="ANIMATION_RANGE", + index=1, + number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key, + ), + _descriptor.EnumValueDescriptor( + name="IMAGE", + index=2, + number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key, + ), + ], + containing_type=None, + serialized_options=None, + serialized_start=393, + serialized_end=447, +) +_sym_db.RegisterEnumDescriptor(_FRAMEREQUEST_PREVIEWMODE) + +_MOBJECTDATA_MOBJECTTYPE = _descriptor.EnumDescriptor( + name="MobjectType", + full_name="frameserver.MobjectData.MobjectType", + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name="VMOBJECT", + index=0, + number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key, + ), + _descriptor.EnumValueDescriptor( + name="IMAGE_MOBJECT", + index=1, + number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key, + ), + ], + containing_type=None, + serialized_options=None, + serialized_start=844, + serialized_end=890, +) +_sym_db.RegisterEnumDescriptor(_MOBJECTDATA_MOBJECTTYPE) + + +_FETCHSCENEDATARESPONSE = _descriptor.Descriptor( + name="FetchSceneDataResponse", + full_name="frameserver.FetchSceneDataResponse", filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name="animation_index", - full_name="frameserver.FrameRequest.animation_index", + name="scene", + full_name="frameserver.FetchSceneDataResponse.scene", index=0, number=1, - type=3, - cpp_type=2, + type=11, + cpp_type=10, label=1, has_default_value=False, - default_value=0, + default_value=None, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + serialized_options=None, + is_extendable=False, + syntax="proto3", + extension_ranges=[], + oneofs=[], + serialized_start=34, + serialized_end=93, +) + + +_SCENE = _descriptor.Descriptor( + name="Scene", + full_name="frameserver.Scene", + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name="name", + full_name="frameserver.Scene.name", + index=0, + number=1, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -45,10 +159,90 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( - name="animation_offset", - full_name="frameserver.FrameRequest.animation_offset", + name="animations", + full_name="frameserver.Scene.animations", + index=1, + number=2, + type=11, + cpp_type=10, + label=3, + has_default_value=False, + default_value=[], + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="background_color", + full_name="frameserver.Scene.background_color", + index=2, + number=3, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=b"".decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + serialized_options=None, + is_extendable=False, + syntax="proto3", + extension_ranges=[], + oneofs=[], + serialized_start=95, + serialized_end=186, +) + + +_ANIMATION = _descriptor.Descriptor( + name="Animation", + full_name="frameserver.Animation", + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name="name", + full_name="frameserver.Animation.name", + index=0, + number=1, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=b"".decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="duration", + full_name="frameserver.Animation.duration", index=1, number=2, type=2, @@ -63,6 +257,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), ], extensions=[], @@ -73,8 +268,127 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=34, - serialized_end=99, + serialized_start=188, + serialized_end=231, +) + + +_FRAMEREQUEST = _descriptor.Descriptor( + name="FrameRequest", + full_name="frameserver.FrameRequest", + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name="time_offset", + full_name="frameserver.FrameRequest.time_offset", + index=0, + number=1, + type=2, + cpp_type=6, + label=1, + has_default_value=False, + default_value=float(0), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="start_index", + full_name="frameserver.FrameRequest.start_index", + index=1, + number=2, + type=5, + cpp_type=1, + label=1, + has_default_value=False, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="end_index", + full_name="frameserver.FrameRequest.end_index", + index=2, + number=3, + type=5, + cpp_type=1, + label=1, + has_default_value=False, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="image_index", + full_name="frameserver.FrameRequest.image_index", + index=3, + number=4, + type=5, + cpp_type=1, + label=1, + has_default_value=False, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="preview_mode", + full_name="frameserver.FrameRequest.preview_mode", + index=4, + number=5, + type=14, + cpp_type=8, + label=1, + has_default_value=False, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + ], + extensions=[], + nested_types=[], + enum_types=[ + _FRAMEREQUEST_PREVIEWMODE, + ], + serialized_options=None, + is_extendable=False, + syntax="proto3", + extension_ranges=[], + oneofs=[], + serialized_start=234, + serialized_end=447, ) @@ -84,6 +398,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name="fill_color", @@ -102,6 +417,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="fill_opacity", @@ -120,6 +436,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="stroke_color", @@ -138,6 +455,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="stroke_opacity", @@ -156,6 +474,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="stroke_width", @@ -174,6 +493,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), ], extensions=[], @@ -184,8 +504,8 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=101, - serialized_end=218, + serialized_start=449, + serialized_end=566, ) @@ -195,6 +515,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name="x", @@ -213,6 +534,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="y", @@ -231,6 +553,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="z", @@ -249,6 +572,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), ], extensions=[], @@ -259,8 +583,8 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=220, - serialized_end=260, + serialized_start=568, + serialized_end=608, ) @@ -270,6 +594,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name="id", @@ -288,17 +613,18 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( - name="points", - full_name="frameserver.MobjectData.points", + name="style", + full_name="frameserver.MobjectData.style", index=1, number=2, type=11, cpp_type=10, - label=3, + label=1, has_default_value=False, - default_value=[], + default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -306,12 +632,51 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( - name="style", - full_name="frameserver.MobjectData.style", + name="type", + full_name="frameserver.MobjectData.type", index=2, number=3, + type=14, + cpp_type=8, + label=1, + has_default_value=False, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="vectorized_mobject_data", + full_name="frameserver.MobjectData.vectorized_mobject_data", + index=3, + number=4, + type=11, + cpp_type=10, + label=1, + has_default_value=False, + default_value=None, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="image_mobject_data", + full_name="frameserver.MobjectData.image_mobject_data", + index=4, + number=5, type=11, cpp_type=10, label=1, @@ -324,12 +689,56 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + ], + extensions=[], + nested_types=[], + enum_types=[ + _MOBJECTDATA_MOBJECTTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax="proto3", + extension_ranges=[], + oneofs=[], + serialized_start=611, + serialized_end=890, +) + + +_VMOBJECTDATA = _descriptor.Descriptor( + name="VMobjectData", + full_name="frameserver.VMobjectData", + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name="points", + full_name="frameserver.VMobjectData.points", + index=0, + number=1, + type=11, + cpp_type=10, + label=3, + has_default_value=False, + default_value=[], + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="needs_redraw", - full_name="frameserver.MobjectData.needs_redraw", - index=3, - number=4, + full_name="frameserver.VMobjectData.needs_redraw", + index=1, + number=2, type=8, cpp_type=7, label=1, @@ -342,6 +751,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), ], extensions=[], @@ -352,8 +762,106 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=262, - serialized_end=380, + serialized_start=892, + serialized_end=964, +) + + +_IMAGEMOBJECTDATA = _descriptor.Descriptor( + name="ImageMobjectData", + full_name="frameserver.ImageMobjectData", + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name="path", + full_name="frameserver.ImageMobjectData.path", + index=0, + number=1, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=b"".decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="height", + full_name="frameserver.ImageMobjectData.height", + index=1, + number=2, + type=2, + cpp_type=6, + label=1, + has_default_value=False, + default_value=float(0), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="width", + full_name="frameserver.ImageMobjectData.width", + index=2, + number=3, + type=2, + cpp_type=6, + label=1, + has_default_value=False, + default_value=float(0), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="center", + full_name="frameserver.ImageMobjectData.center", + index=3, + number=4, + type=11, + cpp_type=10, + label=1, + has_default_value=False, + default_value=None, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + serialized_options=None, + is_extendable=False, + syntax="proto3", + extension_ranges=[], + oneofs=[], + serialized_start=966, + serialized_end=1065, ) @@ -363,6 +871,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name="mobjects", @@ -381,6 +890,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="frame_pending", @@ -399,6 +909,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="animation_finished", @@ -417,6 +928,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="scene_finished", @@ -435,6 +947,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( name="duration", @@ -453,17 +966,37 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( - name="animation_name", - full_name="frameserver.FrameResponse.animation_name", + name="animations", + full_name="frameserver.FrameResponse.animations", index=5, number=6, type=9, cpp_type=9, + label=3, + has_default_value=False, + default_value=[], + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="animation_index", + full_name="frameserver.FrameResponse.animation_index", + index=6, + number=7, + type=5, + cpp_type=1, label=1, has_default_value=False, - default_value=b"".decode("utf-8"), + default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -471,58 +1004,18 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=383, - serialized_end=559, -) - - -_RENDERERSTATUSREQUEST = _descriptor.Descriptor( - name="RendererStatusRequest", - full_name="frameserver.RendererStatusRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=561, - serialized_end=584, -) - - -_RENDERERSTATUSRESPONSE = _descriptor.Descriptor( - name="RendererStatusResponse", - full_name="frameserver.RendererStatusResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ _descriptor.FieldDescriptor( - name="scene_name", - full_name="frameserver.RendererStatusResponse.scene_name", - index=0, - number=1, - type=9, - cpp_type=9, + name="animation_offset", + full_name="frameserver.FrameResponse.animation_offset", + index=7, + number=8, + type=2, + cpp_type=6, label=1, has_default_value=False, - default_value=b"".decode("utf-8"), + default_value=float(0), message_type=None, enum_type=None, containing_type=None, @@ -530,6 +1023,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), ], extensions=[], @@ -540,17 +1034,18 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=586, - serialized_end=630, + serialized_start=1068, + serialized_end=1291, ) -_SCENELOCATIONREQUEST = _descriptor.Descriptor( - name="SceneLocationRequest", - full_name="frameserver.SceneLocationRequest", +_EMPTYREQUEST = _descriptor.Descriptor( + name="EmptyRequest", + full_name="frameserver.EmptyRequest", filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[], extensions=[], nested_types=[], @@ -560,17 +1055,18 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=632, - serialized_end=654, + serialized_start=1293, + serialized_end=1307, ) -_SCENELOCATIONRESPONSE = _descriptor.Descriptor( - name="SceneLocationResponse", - full_name="frameserver.SceneLocationResponse", +_EMPTYRESPONSE = _descriptor.Descriptor( + name="EmptyResponse", + full_name="frameserver.EmptyResponse", filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[], extensions=[], nested_types=[], @@ -580,24 +1076,69 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=656, - serialized_end=679, + serialized_start=1309, + serialized_end=1324, ) -_MOBJECTDATA.fields_by_name["points"].message_type = _POINT +_FETCHSCENEDATARESPONSE.fields_by_name["scene"].message_type = _SCENE +_SCENE.fields_by_name["animations"].message_type = _ANIMATION +_FRAMEREQUEST.fields_by_name["preview_mode"].enum_type = _FRAMEREQUEST_PREVIEWMODE +_FRAMEREQUEST_PREVIEWMODE.containing_type = _FRAMEREQUEST _MOBJECTDATA.fields_by_name["style"].message_type = _STYLE +_MOBJECTDATA.fields_by_name["type"].enum_type = _MOBJECTDATA_MOBJECTTYPE +_MOBJECTDATA.fields_by_name["vectorized_mobject_data"].message_type = _VMOBJECTDATA +_MOBJECTDATA.fields_by_name["image_mobject_data"].message_type = _IMAGEMOBJECTDATA +_MOBJECTDATA_MOBJECTTYPE.containing_type = _MOBJECTDATA +_VMOBJECTDATA.fields_by_name["points"].message_type = _POINT +_IMAGEMOBJECTDATA.fields_by_name["center"].message_type = _POINT _FRAMERESPONSE.fields_by_name["mobjects"].message_type = _MOBJECTDATA +DESCRIPTOR.message_types_by_name["FetchSceneDataResponse"] = _FETCHSCENEDATARESPONSE +DESCRIPTOR.message_types_by_name["Scene"] = _SCENE +DESCRIPTOR.message_types_by_name["Animation"] = _ANIMATION DESCRIPTOR.message_types_by_name["FrameRequest"] = _FRAMEREQUEST DESCRIPTOR.message_types_by_name["Style"] = _STYLE DESCRIPTOR.message_types_by_name["Point"] = _POINT DESCRIPTOR.message_types_by_name["MobjectData"] = _MOBJECTDATA +DESCRIPTOR.message_types_by_name["VMobjectData"] = _VMOBJECTDATA +DESCRIPTOR.message_types_by_name["ImageMobjectData"] = _IMAGEMOBJECTDATA DESCRIPTOR.message_types_by_name["FrameResponse"] = _FRAMERESPONSE -DESCRIPTOR.message_types_by_name["RendererStatusRequest"] = _RENDERERSTATUSREQUEST -DESCRIPTOR.message_types_by_name["RendererStatusResponse"] = _RENDERERSTATUSRESPONSE -DESCRIPTOR.message_types_by_name["SceneLocationRequest"] = _SCENELOCATIONREQUEST -DESCRIPTOR.message_types_by_name["SceneLocationResponse"] = _SCENELOCATIONRESPONSE +DESCRIPTOR.message_types_by_name["EmptyRequest"] = _EMPTYREQUEST +DESCRIPTOR.message_types_by_name["EmptyResponse"] = _EMPTYRESPONSE _sym_db.RegisterFileDescriptor(DESCRIPTOR) +FetchSceneDataResponse = _reflection.GeneratedProtocolMessageType( + "FetchSceneDataResponse", + (_message.Message,), + { + "DESCRIPTOR": _FETCHSCENEDATARESPONSE, + "__module__": "frameserver_pb2" + # @@protoc_insertion_point(class_scope:frameserver.FetchSceneDataResponse) + }, +) +_sym_db.RegisterMessage(FetchSceneDataResponse) + +Scene = _reflection.GeneratedProtocolMessageType( + "Scene", + (_message.Message,), + { + "DESCRIPTOR": _SCENE, + "__module__": "frameserver_pb2" + # @@protoc_insertion_point(class_scope:frameserver.Scene) + }, +) +_sym_db.RegisterMessage(Scene) + +Animation = _reflection.GeneratedProtocolMessageType( + "Animation", + (_message.Message,), + { + "DESCRIPTOR": _ANIMATION, + "__module__": "frameserver_pb2" + # @@protoc_insertion_point(class_scope:frameserver.Animation) + }, +) +_sym_db.RegisterMessage(Animation) + FrameRequest = _reflection.GeneratedProtocolMessageType( "FrameRequest", (_message.Message,), @@ -642,60 +1183,60 @@ ) _sym_db.RegisterMessage(MobjectData) -FrameResponse = _reflection.GeneratedProtocolMessageType( - "FrameResponse", +VMobjectData = _reflection.GeneratedProtocolMessageType( + "VMobjectData", (_message.Message,), { - "DESCRIPTOR": _FRAMERESPONSE, + "DESCRIPTOR": _VMOBJECTDATA, "__module__": "frameserver_pb2" - # @@protoc_insertion_point(class_scope:frameserver.FrameResponse) + # @@protoc_insertion_point(class_scope:frameserver.VMobjectData) }, ) -_sym_db.RegisterMessage(FrameResponse) +_sym_db.RegisterMessage(VMobjectData) -RendererStatusRequest = _reflection.GeneratedProtocolMessageType( - "RendererStatusRequest", +ImageMobjectData = _reflection.GeneratedProtocolMessageType( + "ImageMobjectData", (_message.Message,), { - "DESCRIPTOR": _RENDERERSTATUSREQUEST, + "DESCRIPTOR": _IMAGEMOBJECTDATA, "__module__": "frameserver_pb2" - # @@protoc_insertion_point(class_scope:frameserver.RendererStatusRequest) + # @@protoc_insertion_point(class_scope:frameserver.ImageMobjectData) }, ) -_sym_db.RegisterMessage(RendererStatusRequest) +_sym_db.RegisterMessage(ImageMobjectData) -RendererStatusResponse = _reflection.GeneratedProtocolMessageType( - "RendererStatusResponse", +FrameResponse = _reflection.GeneratedProtocolMessageType( + "FrameResponse", (_message.Message,), { - "DESCRIPTOR": _RENDERERSTATUSRESPONSE, + "DESCRIPTOR": _FRAMERESPONSE, "__module__": "frameserver_pb2" - # @@protoc_insertion_point(class_scope:frameserver.RendererStatusResponse) + # @@protoc_insertion_point(class_scope:frameserver.FrameResponse) }, ) -_sym_db.RegisterMessage(RendererStatusResponse) +_sym_db.RegisterMessage(FrameResponse) -SceneLocationRequest = _reflection.GeneratedProtocolMessageType( - "SceneLocationRequest", +EmptyRequest = _reflection.GeneratedProtocolMessageType( + "EmptyRequest", (_message.Message,), { - "DESCRIPTOR": _SCENELOCATIONREQUEST, + "DESCRIPTOR": _EMPTYREQUEST, "__module__": "frameserver_pb2" - # @@protoc_insertion_point(class_scope:frameserver.SceneLocationRequest) + # @@protoc_insertion_point(class_scope:frameserver.EmptyRequest) }, ) -_sym_db.RegisterMessage(SceneLocationRequest) +_sym_db.RegisterMessage(EmptyRequest) -SceneLocationResponse = _reflection.GeneratedProtocolMessageType( - "SceneLocationResponse", +EmptyResponse = _reflection.GeneratedProtocolMessageType( + "EmptyResponse", (_message.Message,), { - "DESCRIPTOR": _SCENELOCATIONRESPONSE, + "DESCRIPTOR": _EMPTYRESPONSE, "__module__": "frameserver_pb2" - # @@protoc_insertion_point(class_scope:frameserver.SceneLocationResponse) + # @@protoc_insertion_point(class_scope:frameserver.EmptyResponse) }, ) -_sym_db.RegisterMessage(SceneLocationResponse) +_sym_db.RegisterMessage(EmptyResponse) _FRAMESERVER = _descriptor.ServiceDescriptor( @@ -704,8 +1245,9 @@ file=DESCRIPTOR, index=0, serialized_options=None, - serialized_start=682, - serialized_end=953, + create_key=_descriptor._internal_create_key, + serialized_start=1327, + serialized_end=1495, methods=[ _descriptor.MethodDescriptor( name="GetFrameAtTime", @@ -715,24 +1257,17 @@ input_type=_FRAMEREQUEST, output_type=_FRAMERESPONSE, serialized_options=None, + create_key=_descriptor._internal_create_key, ), _descriptor.MethodDescriptor( - name="RendererStatus", - full_name="frameserver.FrameServer.RendererStatus", + name="FetchSceneData", + full_name="frameserver.FrameServer.FetchSceneData", index=1, containing_service=None, - input_type=_RENDERERSTATUSREQUEST, - output_type=_RENDERERSTATUSRESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="UpdateSceneLocation", - full_name="frameserver.FrameServer.UpdateSceneLocation", - index=2, - containing_service=None, - input_type=_SCENELOCATIONREQUEST, - output_type=_SCENELOCATIONRESPONSE, + input_type=_EMPTYREQUEST, + output_type=_FETCHSCENEDATARESPONSE, serialized_options=None, + create_key=_descriptor._internal_create_key, ), ], ) diff --git a/manim/grpc/gen/frameserver_pb2_grpc.py b/manim/grpc/gen/frameserver_pb2_grpc.py index e876f19eaf..26aab8b966 100644 --- a/manim/grpc/gen/frameserver_pb2_grpc.py +++ b/manim/grpc/gen/frameserver_pb2_grpc.py @@ -1,11 +1,12 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" import grpc import frameserver_pb2 as frameserver__pb2 class FrameServerStub(object): - """Missing associated documentation comment in .proto file""" + """Missing associated documentation comment in .proto file.""" def __init__(self, channel): """Constructor. @@ -18,37 +19,24 @@ def __init__(self, channel): request_serializer=frameserver__pb2.FrameRequest.SerializeToString, response_deserializer=frameserver__pb2.FrameResponse.FromString, ) - self.RendererStatus = channel.unary_unary( - "/frameserver.FrameServer/RendererStatus", - request_serializer=frameserver__pb2.RendererStatusRequest.SerializeToString, - response_deserializer=frameserver__pb2.RendererStatusResponse.FromString, - ) - self.UpdateSceneLocation = channel.unary_unary( - "/frameserver.FrameServer/UpdateSceneLocation", - request_serializer=frameserver__pb2.SceneLocationRequest.SerializeToString, - response_deserializer=frameserver__pb2.SceneLocationResponse.FromString, + self.FetchSceneData = channel.unary_unary( + "/frameserver.FrameServer/FetchSceneData", + request_serializer=frameserver__pb2.EmptyRequest.SerializeToString, + response_deserializer=frameserver__pb2.FetchSceneDataResponse.FromString, ) class FrameServerServicer(object): - """Missing associated documentation comment in .proto file""" + """Missing associated documentation comment in .proto file.""" def GetFrameAtTime(self, request, context): - """Updates the scene to the specified animation offset and returns a - serialization of the frame at that time. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def RendererStatus(self, request, context): - """Used to signal to the renderer that manim is running.""" + """Returns a serialization of the scene at the specified time.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") - def UpdateSceneLocation(self, request, context): - """Missing associated documentation comment in .proto file""" + def FetchSceneData(self, request, context): + """Returns a list of the names and durations of all animations in the scene.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") @@ -61,15 +49,10 @@ def add_FrameServerServicer_to_server(servicer, server): request_deserializer=frameserver__pb2.FrameRequest.FromString, response_serializer=frameserver__pb2.FrameResponse.SerializeToString, ), - "RendererStatus": grpc.unary_unary_rpc_method_handler( - servicer.RendererStatus, - request_deserializer=frameserver__pb2.RendererStatusRequest.FromString, - response_serializer=frameserver__pb2.RendererStatusResponse.SerializeToString, - ), - "UpdateSceneLocation": grpc.unary_unary_rpc_method_handler( - servicer.UpdateSceneLocation, - request_deserializer=frameserver__pb2.SceneLocationRequest.FromString, - response_serializer=frameserver__pb2.SceneLocationResponse.SerializeToString, + "FetchSceneData": grpc.unary_unary_rpc_method_handler( + servicer.FetchSceneData, + request_deserializer=frameserver__pb2.EmptyRequest.FromString, + response_serializer=frameserver__pb2.FetchSceneDataResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -80,7 +63,7 @@ def add_FrameServerServicer_to_server(servicer, server): # This class is part of an EXPERIMENTAL API. class FrameServer(object): - """Missing associated documentation comment in .proto file""" + """Missing associated documentation comment in .proto file.""" @staticmethod def GetFrameAtTime( @@ -89,6 +72,7 @@ def GetFrameAtTime( options=(), channel_credentials=None, call_credentials=None, + insecure=False, compression=None, wait_for_ready=None, timeout=None, @@ -102,6 +86,7 @@ def GetFrameAtTime( frameserver__pb2.FrameResponse.FromString, options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, @@ -110,39 +95,13 @@ def GetFrameAtTime( ) @staticmethod - def RendererStatus( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/frameserver.FrameServer/RendererStatus", - frameserver__pb2.RendererStatusRequest.SerializeToString, - frameserver__pb2.RendererStatusResponse.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - @staticmethod - def UpdateSceneLocation( + def FetchSceneData( request, target, options=(), channel_credentials=None, call_credentials=None, + insecure=False, compression=None, wait_for_ready=None, timeout=None, @@ -151,11 +110,12 @@ def UpdateSceneLocation( return grpc.experimental.unary_unary( request, target, - "/frameserver.FrameServer/UpdateSceneLocation", - frameserver__pb2.SceneLocationRequest.SerializeToString, - frameserver__pb2.SceneLocationResponse.FromString, + "/frameserver.FrameServer/FetchSceneData", + frameserver__pb2.EmptyRequest.SerializeToString, + frameserver__pb2.FetchSceneDataResponse.FromString, options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, diff --git a/manim/grpc/gen/renderserver_pb2.py b/manim/grpc/gen/renderserver_pb2.py index 20dcb998fc..ad29d68591 100644 --- a/manim/grpc/gen/renderserver_pb2.py +++ b/manim/grpc/gen/renderserver_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: renderserver.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -17,67 +17,29 @@ package="renderserver", syntax="proto3", serialized_options=None, - serialized_pb=b'\n\x12renderserver.proto\x12\x0crenderserver"\x0e\n\x0c\x45mptyRequest"\x0f\n\rEmptyResponse"\x1f\n\x0fNewSceneRequest\x12\x0c\n\x04name\x18\x01 \x01(\t"\x12\n\x10NewSceneResponse"+\n\tAnimation\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08\x64uration\x18\x02 \x01(\x02"\x18\n\x16\x41nimationStatusRequest"\x19\n\x17\x41nimationStatusResponse"m\n\x12ManimStatusRequest\x12\x12\n\nscene_name\x18\x01 \x01(\t\x12\x16\n\x0escene_finished\x18\x02 \x01(\x08\x12+\n\nanimations\x18\x03 \x03(\x0b\x32\x17.renderserver.Animation"\x15\n\x13ManimStatusResponse"\x14\n\x12UpdateSceneRequest"\x15\n\x13UpdateSceneResponse2\x94\x03\n\x0cRenderServer\x12J\n\x0f\x41nimationStatus\x12\x1a.renderserver.EmptyRequest\x1a\x1b.renderserver.EmptyResponse\x12R\n\x0bManimStatus\x12 .renderserver.ManimStatusRequest\x1a!.renderserver.ManimStatusResponse\x12R\n\x0bUpdateScene\x12 .renderserver.UpdateSceneRequest\x1a!.renderserver.UpdateSceneResponse\x12\x46\n\x08NewScene\x12\x1d.renderserver.NewSceneRequest\x1a\x1b.renderserver.EmptyResponse\x12H\n\rSceneFinished\x12\x1a.renderserver.EmptyRequest\x1a\x1b.renderserver.EmptyResponseb\x06proto3', -) - - -_EMPTYREQUEST = _descriptor.Descriptor( - name="EmptyRequest", - full_name="renderserver.EmptyRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=36, - serialized_end=50, -) - - -_EMPTYRESPONSE = _descriptor.Descriptor( - name="EmptyResponse", - full_name="renderserver.EmptyResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=52, - serialized_end=67, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x12renderserver.proto\x12\x0crenderserver"f\n\x16UpdateSceneDataRequest\x12"\n\x05scene\x18\x01 \x01(\x0b\x32\x13.renderserver.Scene\x12\x11\n\texception\x18\x02 \x01(\t\x12\x15\n\rhas_exception\x18\x03 \x01(\x08"\\\n\x05Scene\x12\x0c\n\x04name\x18\x01 \x01(\t\x12+\n\nanimations\x18\x02 \x03(\x0b\x32\x17.renderserver.Animation\x12\x18\n\x10\x62\x61\x63kground_color\x18\x03 \x01(\t"+\n\tAnimation\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08\x64uration\x18\x02 \x01(\x02"\x0e\n\x0c\x45mptyRequest"\x0f\n\rEmptyResponse2d\n\x0cRenderServer\x12T\n\x0fUpdateSceneData\x12$.renderserver.UpdateSceneDataRequest\x1a\x1b.renderserver.EmptyResponseb\x06proto3', ) -_NEWSCENEREQUEST = _descriptor.Descriptor( - name="NewSceneRequest", - full_name="renderserver.NewSceneRequest", +_UPDATESCENEDATAREQUEST = _descriptor.Descriptor( + name="UpdateSceneDataRequest", + full_name="renderserver.UpdateSceneDataRequest", filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name="name", - full_name="renderserver.NewSceneRequest.name", + name="scene", + full_name="renderserver.UpdateSceneDataRequest.scene", index=0, number=1, - type=9, - cpp_type=9, + type=11, + cpp_type=10, label=1, has_default_value=False, - default_value=b"".decode("utf-8"), + default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -85,53 +47,13 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=69, - serialized_end=100, -) - - -_NEWSCENERESPONSE = _descriptor.Descriptor( - name="NewSceneResponse", - full_name="renderserver.NewSceneResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=102, - serialized_end=120, -) - - -_ANIMATION = _descriptor.Descriptor( - name="Animation", - full_name="renderserver.Animation", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ _descriptor.FieldDescriptor( - name="name", - full_name="renderserver.Animation.name", - index=0, - number=1, + name="exception", + full_name="renderserver.UpdateSceneDataRequest.exception", + index=1, + number=2, type=9, cpp_type=9, label=1, @@ -144,17 +66,18 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( - name="duration", - full_name="renderserver.Animation.duration", - index=1, - number=2, - type=2, - cpp_type=6, + name="has_exception", + full_name="renderserver.UpdateSceneDataRequest.has_exception", + index=2, + number=3, + type=8, + cpp_type=7, label=1, has_default_value=False, - default_value=float(0), + default_value=False, message_type=None, enum_type=None, containing_type=None, @@ -162,6 +85,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), ], extensions=[], @@ -172,61 +96,22 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=122, - serialized_end=165, -) - - -_ANIMATIONSTATUSREQUEST = _descriptor.Descriptor( - name="AnimationStatusRequest", - full_name="renderserver.AnimationStatusRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=167, - serialized_end=191, -) - - -_ANIMATIONSTATUSRESPONSE = _descriptor.Descriptor( - name="AnimationStatusResponse", - full_name="renderserver.AnimationStatusResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=193, - serialized_end=218, + serialized_start=36, + serialized_end=138, ) -_MANIMSTATUSREQUEST = _descriptor.Descriptor( - name="ManimStatusRequest", - full_name="renderserver.ManimStatusRequest", +_SCENE = _descriptor.Descriptor( + name="Scene", + full_name="renderserver.Scene", filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name="scene_name", - full_name="renderserver.ManimStatusRequest.scene_name", + name="name", + full_name="renderserver.Scene.name", index=0, number=1, type=9, @@ -241,17 +126,18 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( - name="scene_finished", - full_name="renderserver.ManimStatusRequest.scene_finished", + name="animations", + full_name="renderserver.Scene.animations", index=1, number=2, - type=8, - cpp_type=7, - label=1, + type=11, + cpp_type=10, + label=3, has_default_value=False, - default_value=False, + default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -259,17 +145,18 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), _descriptor.FieldDescriptor( - name="animations", - full_name="renderserver.ManimStatusRequest.animations", + name="background_color", + full_name="renderserver.Scene.background_color", index=2, number=3, - type=11, - cpp_type=10, - label=3, + type=9, + cpp_type=9, + label=1, has_default_value=False, - default_value=[], + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -277,6 +164,7 @@ extension_scope=None, serialized_options=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, ), ], extensions=[], @@ -287,18 +175,58 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=220, - serialized_end=329, + serialized_start=140, + serialized_end=232, ) -_MANIMSTATUSRESPONSE = _descriptor.Descriptor( - name="ManimStatusResponse", - full_name="renderserver.ManimStatusResponse", +_ANIMATION = _descriptor.Descriptor( + name="Animation", + full_name="renderserver.Animation", filename=None, file=DESCRIPTOR, containing_type=None, - fields=[], + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name="name", + full_name="renderserver.Animation.name", + index=0, + number=1, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=b"".decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + _descriptor.FieldDescriptor( + name="duration", + full_name="renderserver.Animation.duration", + index=1, + number=2, + type=2, + cpp_type=6, + label=1, + has_default_value=False, + default_value=float(0), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + ), + ], extensions=[], nested_types=[], enum_types=[], @@ -307,17 +235,18 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=331, - serialized_end=352, + serialized_start=234, + serialized_end=277, ) -_UPDATESCENEREQUEST = _descriptor.Descriptor( - name="UpdateSceneRequest", - full_name="renderserver.UpdateSceneRequest", +_EMPTYREQUEST = _descriptor.Descriptor( + name="EmptyRequest", + full_name="renderserver.EmptyRequest", filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[], extensions=[], nested_types=[], @@ -327,17 +256,18 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=354, - serialized_end=374, + serialized_start=279, + serialized_end=293, ) -_UPDATESCENERESPONSE = _descriptor.Descriptor( - name="UpdateSceneResponse", - full_name="renderserver.UpdateSceneResponse", +_EMPTYRESPONSE = _descriptor.Descriptor( + name="EmptyResponse", + full_name="renderserver.EmptyResponse", filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[], extensions=[], nested_types=[], @@ -347,67 +277,40 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=376, - serialized_end=397, + serialized_start=295, + serialized_end=310, ) -_MANIMSTATUSREQUEST.fields_by_name["animations"].message_type = _ANIMATION +_UPDATESCENEDATAREQUEST.fields_by_name["scene"].message_type = _SCENE +_SCENE.fields_by_name["animations"].message_type = _ANIMATION +DESCRIPTOR.message_types_by_name["UpdateSceneDataRequest"] = _UPDATESCENEDATAREQUEST +DESCRIPTOR.message_types_by_name["Scene"] = _SCENE +DESCRIPTOR.message_types_by_name["Animation"] = _ANIMATION DESCRIPTOR.message_types_by_name["EmptyRequest"] = _EMPTYREQUEST DESCRIPTOR.message_types_by_name["EmptyResponse"] = _EMPTYRESPONSE -DESCRIPTOR.message_types_by_name["NewSceneRequest"] = _NEWSCENEREQUEST -DESCRIPTOR.message_types_by_name["NewSceneResponse"] = _NEWSCENERESPONSE -DESCRIPTOR.message_types_by_name["Animation"] = _ANIMATION -DESCRIPTOR.message_types_by_name["AnimationStatusRequest"] = _ANIMATIONSTATUSREQUEST -DESCRIPTOR.message_types_by_name["AnimationStatusResponse"] = _ANIMATIONSTATUSRESPONSE -DESCRIPTOR.message_types_by_name["ManimStatusRequest"] = _MANIMSTATUSREQUEST -DESCRIPTOR.message_types_by_name["ManimStatusResponse"] = _MANIMSTATUSRESPONSE -DESCRIPTOR.message_types_by_name["UpdateSceneRequest"] = _UPDATESCENEREQUEST -DESCRIPTOR.message_types_by_name["UpdateSceneResponse"] = _UPDATESCENERESPONSE _sym_db.RegisterFileDescriptor(DESCRIPTOR) -EmptyRequest = _reflection.GeneratedProtocolMessageType( - "EmptyRequest", - (_message.Message,), - { - "DESCRIPTOR": _EMPTYREQUEST, - "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.EmptyRequest) - }, -) -_sym_db.RegisterMessage(EmptyRequest) - -EmptyResponse = _reflection.GeneratedProtocolMessageType( - "EmptyResponse", +UpdateSceneDataRequest = _reflection.GeneratedProtocolMessageType( + "UpdateSceneDataRequest", (_message.Message,), { - "DESCRIPTOR": _EMPTYRESPONSE, + "DESCRIPTOR": _UPDATESCENEDATAREQUEST, "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.EmptyResponse) + # @@protoc_insertion_point(class_scope:renderserver.UpdateSceneDataRequest) }, ) -_sym_db.RegisterMessage(EmptyResponse) +_sym_db.RegisterMessage(UpdateSceneDataRequest) -NewSceneRequest = _reflection.GeneratedProtocolMessageType( - "NewSceneRequest", +Scene = _reflection.GeneratedProtocolMessageType( + "Scene", (_message.Message,), { - "DESCRIPTOR": _NEWSCENEREQUEST, + "DESCRIPTOR": _SCENE, "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.NewSceneRequest) + # @@protoc_insertion_point(class_scope:renderserver.Scene) }, ) -_sym_db.RegisterMessage(NewSceneRequest) - -NewSceneResponse = _reflection.GeneratedProtocolMessageType( - "NewSceneResponse", - (_message.Message,), - { - "DESCRIPTOR": _NEWSCENERESPONSE, - "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.NewSceneResponse) - }, -) -_sym_db.RegisterMessage(NewSceneResponse) +_sym_db.RegisterMessage(Scene) Animation = _reflection.GeneratedProtocolMessageType( "Animation", @@ -420,71 +323,27 @@ ) _sym_db.RegisterMessage(Animation) -AnimationStatusRequest = _reflection.GeneratedProtocolMessageType( - "AnimationStatusRequest", - (_message.Message,), - { - "DESCRIPTOR": _ANIMATIONSTATUSREQUEST, - "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.AnimationStatusRequest) - }, -) -_sym_db.RegisterMessage(AnimationStatusRequest) - -AnimationStatusResponse = _reflection.GeneratedProtocolMessageType( - "AnimationStatusResponse", - (_message.Message,), - { - "DESCRIPTOR": _ANIMATIONSTATUSRESPONSE, - "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.AnimationStatusResponse) - }, -) -_sym_db.RegisterMessage(AnimationStatusResponse) - -ManimStatusRequest = _reflection.GeneratedProtocolMessageType( - "ManimStatusRequest", - (_message.Message,), - { - "DESCRIPTOR": _MANIMSTATUSREQUEST, - "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.ManimStatusRequest) - }, -) -_sym_db.RegisterMessage(ManimStatusRequest) - -ManimStatusResponse = _reflection.GeneratedProtocolMessageType( - "ManimStatusResponse", - (_message.Message,), - { - "DESCRIPTOR": _MANIMSTATUSRESPONSE, - "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.ManimStatusResponse) - }, -) -_sym_db.RegisterMessage(ManimStatusResponse) - -UpdateSceneRequest = _reflection.GeneratedProtocolMessageType( - "UpdateSceneRequest", +EmptyRequest = _reflection.GeneratedProtocolMessageType( + "EmptyRequest", (_message.Message,), { - "DESCRIPTOR": _UPDATESCENEREQUEST, + "DESCRIPTOR": _EMPTYREQUEST, "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.UpdateSceneRequest) + # @@protoc_insertion_point(class_scope:renderserver.EmptyRequest) }, ) -_sym_db.RegisterMessage(UpdateSceneRequest) +_sym_db.RegisterMessage(EmptyRequest) -UpdateSceneResponse = _reflection.GeneratedProtocolMessageType( - "UpdateSceneResponse", +EmptyResponse = _reflection.GeneratedProtocolMessageType( + "EmptyResponse", (_message.Message,), { - "DESCRIPTOR": _UPDATESCENERESPONSE, + "DESCRIPTOR": _EMPTYRESPONSE, "__module__": "renderserver_pb2" - # @@protoc_insertion_point(class_scope:renderserver.UpdateSceneResponse) + # @@protoc_insertion_point(class_scope:renderserver.EmptyResponse) }, ) -_sym_db.RegisterMessage(UpdateSceneResponse) +_sym_db.RegisterMessage(EmptyResponse) _RENDERSERVER = _descriptor.ServiceDescriptor( @@ -493,53 +352,19 @@ file=DESCRIPTOR, index=0, serialized_options=None, - serialized_start=400, - serialized_end=804, + create_key=_descriptor._internal_create_key, + serialized_start=312, + serialized_end=412, methods=[ _descriptor.MethodDescriptor( - name="AnimationStatus", - full_name="renderserver.RenderServer.AnimationStatus", + name="UpdateSceneData", + full_name="renderserver.RenderServer.UpdateSceneData", index=0, containing_service=None, - input_type=_EMPTYREQUEST, - output_type=_EMPTYRESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="ManimStatus", - full_name="renderserver.RenderServer.ManimStatus", - index=1, - containing_service=None, - input_type=_MANIMSTATUSREQUEST, - output_type=_MANIMSTATUSRESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="UpdateScene", - full_name="renderserver.RenderServer.UpdateScene", - index=2, - containing_service=None, - input_type=_UPDATESCENEREQUEST, - output_type=_UPDATESCENERESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="NewScene", - full_name="renderserver.RenderServer.NewScene", - index=3, - containing_service=None, - input_type=_NEWSCENEREQUEST, - output_type=_EMPTYRESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="SceneFinished", - full_name="renderserver.RenderServer.SceneFinished", - index=4, - containing_service=None, - input_type=_EMPTYREQUEST, + input_type=_UPDATESCENEDATAREQUEST, output_type=_EMPTYRESPONSE, serialized_options=None, + create_key=_descriptor._internal_create_key, ), ], ) diff --git a/manim/grpc/gen/renderserver_pb2_grpc.py b/manim/grpc/gen/renderserver_pb2_grpc.py index 26feaa1dd6..0a33d96835 100644 --- a/manim/grpc/gen/renderserver_pb2_grpc.py +++ b/manim/grpc/gen/renderserver_pb2_grpc.py @@ -1,11 +1,12 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" import grpc import renderserver_pb2 as renderserver__pb2 class RenderServerStub(object): - """Missing associated documentation comment in .proto file""" + """Missing associated documentation comment in .proto file.""" def __init__(self, channel): """Constructor. @@ -13,62 +14,18 @@ def __init__(self, channel): Args: channel: A grpc.Channel. """ - self.AnimationStatus = channel.unary_unary( - "/renderserver.RenderServer/AnimationStatus", - request_serializer=renderserver__pb2.EmptyRequest.SerializeToString, - response_deserializer=renderserver__pb2.EmptyResponse.FromString, - ) - self.ManimStatus = channel.unary_unary( - "/renderserver.RenderServer/ManimStatus", - request_serializer=renderserver__pb2.ManimStatusRequest.SerializeToString, - response_deserializer=renderserver__pb2.ManimStatusResponse.FromString, - ) - self.UpdateScene = channel.unary_unary( - "/renderserver.RenderServer/UpdateScene", - request_serializer=renderserver__pb2.UpdateSceneRequest.SerializeToString, - response_deserializer=renderserver__pb2.UpdateSceneResponse.FromString, - ) - self.NewScene = channel.unary_unary( - "/renderserver.RenderServer/NewScene", - request_serializer=renderserver__pb2.NewSceneRequest.SerializeToString, - response_deserializer=renderserver__pb2.EmptyResponse.FromString, - ) - self.SceneFinished = channel.unary_unary( - "/renderserver.RenderServer/SceneFinished", - request_serializer=renderserver__pb2.EmptyRequest.SerializeToString, + self.UpdateSceneData = channel.unary_unary( + "/renderserver.RenderServer/UpdateSceneData", + request_serializer=renderserver__pb2.UpdateSceneDataRequest.SerializeToString, response_deserializer=renderserver__pb2.EmptyResponse.FromString, ) class RenderServerServicer(object): - """Missing associated documentation comment in .proto file""" - - def AnimationStatus(self, request, context): - """Used to signal that a new animation is ready to be animated.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") + """Missing associated documentation comment in .proto file.""" - def ManimStatus(self, request, context): - """Used to signal when manim starts and stops.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def UpdateScene(self, request, context): - """Missing associated documentation comment in .proto file""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def NewScene(self, request, context): - """Missing associated documentation comment in .proto file""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def SceneFinished(self, request, context): - """Missing associated documentation comment in .proto file""" + def UpdateSceneData(self, request, context): + """Called from Manim when a scene has been newly rendered.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") @@ -76,29 +33,9 @@ def SceneFinished(self, request, context): def add_RenderServerServicer_to_server(servicer, server): rpc_method_handlers = { - "AnimationStatus": grpc.unary_unary_rpc_method_handler( - servicer.AnimationStatus, - request_deserializer=renderserver__pb2.EmptyRequest.FromString, - response_serializer=renderserver__pb2.EmptyResponse.SerializeToString, - ), - "ManimStatus": grpc.unary_unary_rpc_method_handler( - servicer.ManimStatus, - request_deserializer=renderserver__pb2.ManimStatusRequest.FromString, - response_serializer=renderserver__pb2.ManimStatusResponse.SerializeToString, - ), - "UpdateScene": grpc.unary_unary_rpc_method_handler( - servicer.UpdateScene, - request_deserializer=renderserver__pb2.UpdateSceneRequest.FromString, - response_serializer=renderserver__pb2.UpdateSceneResponse.SerializeToString, - ), - "NewScene": grpc.unary_unary_rpc_method_handler( - servicer.NewScene, - request_deserializer=renderserver__pb2.NewSceneRequest.FromString, - response_serializer=renderserver__pb2.EmptyResponse.SerializeToString, - ), - "SceneFinished": grpc.unary_unary_rpc_method_handler( - servicer.SceneFinished, - request_deserializer=renderserver__pb2.EmptyRequest.FromString, + "UpdateSceneData": grpc.unary_unary_rpc_method_handler( + servicer.UpdateSceneData, + request_deserializer=renderserver__pb2.UpdateSceneDataRequest.FromString, response_serializer=renderserver__pb2.EmptyResponse.SerializeToString, ), } @@ -110,123 +47,16 @@ def add_RenderServerServicer_to_server(servicer, server): # This class is part of an EXPERIMENTAL API. class RenderServer(object): - """Missing associated documentation comment in .proto file""" - - @staticmethod - def AnimationStatus( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/renderserver.RenderServer/AnimationStatus", - renderserver__pb2.EmptyRequest.SerializeToString, - renderserver__pb2.EmptyResponse.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - @staticmethod - def ManimStatus( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/renderserver.RenderServer/ManimStatus", - renderserver__pb2.ManimStatusRequest.SerializeToString, - renderserver__pb2.ManimStatusResponse.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - @staticmethod - def UpdateScene( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/renderserver.RenderServer/UpdateScene", - renderserver__pb2.UpdateSceneRequest.SerializeToString, - renderserver__pb2.UpdateSceneResponse.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - @staticmethod - def NewScene( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/renderserver.RenderServer/NewScene", - renderserver__pb2.NewSceneRequest.SerializeToString, - renderserver__pb2.EmptyResponse.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) + """Missing associated documentation comment in .proto file.""" @staticmethod - def SceneFinished( + def UpdateSceneData( request, target, options=(), channel_credentials=None, call_credentials=None, + insecure=False, compression=None, wait_for_ready=None, timeout=None, @@ -235,11 +65,12 @@ def SceneFinished( return grpc.experimental.unary_unary( request, target, - "/renderserver.RenderServer/SceneFinished", - renderserver__pb2.EmptyRequest.SerializeToString, + "/renderserver.RenderServer/UpdateSceneData", + renderserver__pb2.UpdateSceneDataRequest.SerializeToString, renderserver__pb2.EmptyResponse.FromString, options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, diff --git a/manim/grpc/impl/frame_server_impl.py b/manim/grpc/impl/frame_server_impl.py index 50a2524080..7f55799284 100644 --- a/manim/grpc/impl/frame_server_impl.py +++ b/manim/grpc/impl/frame_server_impl.py @@ -10,250 +10,287 @@ import subprocess as sp import threading import time -import ctypes +import traceback from ...utils.module_ops import ( get_module, get_scene_classes_from_module, get_scenes_to_render, + scene_classes_from_file, ) from ... import logger from ...constants import JS_RENDERER_INFO +from ...renderer.js_renderer import JsRenderer +from ...utils.family import extract_mobject_family_members +import logging +import copy +from ...mobject.value_tracker import ValueTracker +from ...mobject.types.vectorized_mobject import VMobject +from ...mobject.types.image_mobject import ImageMobject -class FrameServer(frameserver_pb2_grpc.FrameServerServicer): - def animation_index_is_cached(self, animation_index): - return animation_index < len(self.keyframes) +class ScriptUpdateHandler(FileSystemEventHandler): + def __init__(self, frame_server): + super().__init__() + self.frame_server = frame_server - def __init__(self, server, scene_class): - self.server = server - self.keyframes = [] - self.scene = scene_class(self) - self.scene_thread = threading.Thread( - target=lambda s: s.render(), args=(self.scene,) - ) - self.previous_frame_animation_index = None - self.scene_finished = False - - path = "./example_scenes/basic.py" - event_handler = UpdateFrontendHandler(self) - observer = Observer() - observer.schedule(event_handler, path) - observer.start() + def catch_all_handler(self, event): + print(event) + + def on_moved(self, event): + self.catch_all_handler(event) + + def on_created(self, event): + self.catch_all_handler(event) + + def on_deleted(self, event): + self.catch_all_handler(event) + + def on_modified(self, event): + self.frame_server.load_scene_module() - # If a javascript renderer is running, notify it of the scene being served. If - # not, spawn one and it will request the scene when it starts. with grpc.insecure_channel("localhost:50052") as channel: stub = renderserver_pb2_grpc.RenderServerStub(channel) - request = renderserver_pb2.NewSceneRequest(name=str(self.scene)) try: - stub.NewScene(request) + self.frame_server.update_renderer_scene_data() except grpc._channel._InactiveRpcError: - logger.warning(f"No frontend was detected at localhost:50052.") - try: - sp.Popen(config["js_renderer_path"]) - except PermissionError: - logger.info(JS_RENDERER_INFO) - self.server.stop(None) - return + logger.warning("No frontend was detected at localhost:50052.") + sp.Popen(config["js_renderer_path"]) - self.scene_thread.start() - def signal_pending_animation(self, animation_index): - self.scene.start_animation = animation_index - self.scene.animation_finished.set() - return frameserver_pb2.FrameResponse(frame_pending=True) +class FrameServer(frameserver_pb2_grpc.FrameServerServicer): + def __init__(self, server, input_file_path): + self.server = server + self.input_file_path = input_file_path + self.exception = None + self.load_scene_module() + + observer = Observer() + event_handler = ScriptUpdateHandler(self) + path = self.input_file_path + observer.schedule(event_handler, path) + observer.start() # When / where to stop? + + try: + self.update_renderer_scene_data() + except grpc._channel._InactiveRpcError: + logger.warning("No frontend was detected at localhost:50052.") + try: + sp.Popen(config["js_renderer_path"]) + except PermissionError: + logger.info(JS_RENDERER_INFO) + self.server.stop(None) + return def GetFrameAtTime(self, request, context): - selected_scene = None - if self.animation_index_is_cached(request.animation_index): - selected_scene = self.keyframes[request.animation_index] - else: - return self.signal_pending_animation(request.animation_index) - - # play() uses run_time and wait() uses duration TODO: Fix this inconsistency. - # TODO: What about animations without a fixed duration? - duration = ( - selected_scene.run_time - if selected_scene.animations - else selected_scene.duration - ) - - if request.animation_offset > duration: - if self.animation_index_is_cached(request.animation_index + 1): - # TODO: Clone scenes to allow reuse. - selected_scene = self.keyframes[request.animation_index + 1] + try: + # Determine start and end indices. + if ( + request.preview_mode + == frameserver_pb2.FrameRequest.PreviewMode.ANIMATION_RANGE + ): + requested_scene_index = request.start_index + elif request.preview_mode == frameserver_pb2.FrameRequest.PreviewMode.ALL: + requested_scene_index = 0 + elif request.preview_mode == frameserver_pb2.FrameRequest.PreviewMode.IMAGE: + requested_scene_index = request.image_index + + if ( + request.preview_mode + == frameserver_pb2.FrameRequest.PreviewMode.ANIMATION_RANGE + and request.end_index > request.start_index + ): + requested_end_index = request.end_index + elif request.preview_mode == frameserver_pb2.FrameRequest.PreviewMode.ALL: + requested_end_index = len(self.keyframes) + elif request.preview_mode == frameserver_pb2.FrameRequest.PreviewMode.IMAGE: + requested_end_index = len(self.keyframes) + + # Find the requested scene. + requested_scene = self.keyframes[requested_scene_index] + requested_scene_end_time = requested_scene.duration + scene_finished = False + while requested_scene_end_time < request.time_offset: + if requested_scene_index + 1 < requested_end_index: + requested_scene_index += 1 + requested_scene = self.keyframes[requested_scene_index] + requested_scene_end_time += requested_scene.duration + else: + scene_finished = True + break + + if requested_scene_index == self.previous_scene_index: + requested_scene = self.previous_scene else: - return self.signal_pending_animation(request.animation_index + 1) + requested_scene = copy.deepcopy(requested_scene) + self.previous_scene = requested_scene + self.previous_scene_index = requested_scene_index - setattr(selected_scene, "camera", self.scene.camera) + # Update to the requested time. + if not scene_finished: + requested_scene_start_time = ( + requested_scene_end_time - requested_scene.duration + ) + animation_offset = request.time_offset - requested_scene_start_time + else: + animation_offset = requested_scene.duration + requested_scene.update_to_time(animation_offset) - if selected_scene.animations: - # This is a call to play(). - selected_scene.update_animation_to_time(request.animation_offset) - selected_scene.update_frame( - selected_scene.moving_mobjects, - selected_scene.static_image, - ) - serialized_mobject_list, duration = selected_scene.add_frame( - selected_scene.renderer.get_frame() + # Serialize the scene's mobjects. + mobjects = extract_mobject_family_members( + requested_scene.mobjects, only_those_with_points=True ) - resp = list_to_frame_response( - selected_scene, duration, serialized_mobject_list + serialized_mobjects = [ + serialize_mobject(mobject) + for mobject in mobjects + if not isinstance(mobject, ValueTracker) + ] + + resp = frameserver_pb2.FrameResponse( + mobjects=serialized_mobjects, + frame_pending=False, + animation_finished=False, + scene_finished=scene_finished + or request.preview_mode + == frameserver_pb2.FrameRequest.PreviewMode.IMAGE, + duration=requested_scene.duration, + animations=map( + lambda anim: anim.__class__.__name__, requested_scene.animations + ), + animation_index=requested_scene_index, + animation_offset=animation_offset, ) return resp - else: - # This is a call to wait(). - if selected_scene.should_update_mobjects(): - # TODO, be smart about setting a static image - # the same way Scene.play does - selected_scene.update_animation_to_time(time) - selected_scene.update_frame() - serialized_mobject_list, duration = selected_scene.add_frame( - selected_scene.get_frame() - ) - frame_response = list_to_frame_response( - selected_scene, duration, serialized_mobject_list + except Exception as e: + traceback.print_exc() + + def FetchSceneData(self, request, context): + try: + request = frameserver_pb2.FetchSceneDataResponse( + scene=frameserver_pb2.Scene( + name=str(self.scene), + animations=[ + frameserver_pb2.Animation( + name=animations_to_name(scene.animations), + duration=scene.duration, + ) + for scene in self.keyframes + ], + ), + ) + if hasattr(self.scene.camera, "background_color"): + request.scene.background_color = self.scene.camera.background_color + else: + request.scene.background_color = "#000000" + return request + except Exception as e: + traceback.print_exc() + + def load_scene_module(self): + self.exception = None + try: + self.scene_class = scene_classes_from_file( + self.input_file_path, require_single_scene=True + ) + self.generate_keyframe_data() + except Exception as e: + self.exception = e + + def generate_keyframe_data(self): + self.keyframes = [] + self.previous_scene_index = None + self.previous_scene = None + self.renderer = JsRenderer(self) + self.scene = self.scene_class(self.renderer) + self.scene.render() + + def update_renderer_scene_data(self): + # If a javascript renderer is running, notify it of the scene being served. If + # not, spawn one and it will request the scene when it starts. + with grpc.insecure_channel("localhost:50052") as channel: + stub = renderserver_pb2_grpc.RenderServerStub(channel) + if not self.exception: + request = renderserver_pb2.UpdateSceneDataRequest( + scene=renderserver_pb2.Scene( + name=str(self.scene), + animations=[ + renderserver_pb2.Animation( + name=animations_to_name(scene.animations), + duration=scene.duration, + ) + for scene in self.keyframes + ], + ), ) - if ( - selected_scene.stop_condition is not None - and selected_scene.stop_condition() - ): - selected_scene.animation_finished.set() - frame_response.frame_pending = True - selected_scene.renderer_waiting = True - return frame_response - elif selected_scene.renderer.skip_animations: - # Do nothing - return + if hasattr(self.scene.camera, "background_color"): + request.scene.background_color = self.scene.camera.background_color + else: + request.scene.background_color = "#000000" else: - selected_scene.update_frame() - dt = 1 / selected_scene.camera.frame_rate - serialized_mobject_list, duration = selected_scene.add_frame( - selected_scene.get_frame(), - num_frames=int(selected_scene.duration / dt), + lines = traceback.format_exception( + None, self.exception, self.exception.__traceback__ ) - resp = list_to_frame_response( - selected_scene, duration, serialized_mobject_list + request = renderserver_pb2.UpdateSceneDataRequest( + has_exception=True, exception="\n".join(lines) ) - return resp + stub.UpdateSceneData(request) - def RendererStatus(self, request, context): - response = frameserver_pb2.RendererStatusResponse() - response.scene_name = str(self.scene) - return response - # def UpdateSceneLocation(self, request, context): - # # Reload self.scene. - # print(scene_classes_to_render) +def animations_to_name(animations): + if len(animations) == 1: + return str(animations[0].__class__.__name__) + return f"{str(animations[0])}..." - # response = frameserver_pb2.SceneLocationResponse() - # return response +def serialize_mobject(mobject): + mob_proto = frameserver_pb2.MobjectData() -def list_to_frame_response(scene, duration, serialized_mobject_list): - response = frameserver_pb2.FrameResponse() - response.frame_pending = False - response.duration = duration + if isinstance(mobject, VMobject): + needs_redraw = False + point_hash = hash(tuple(mobject.points.flatten())) + if mobject.point_hash != point_hash: + mobject.point_hash = point_hash + needs_redraw = True + mob_proto.vectorized_mobject_data.needs_redraw = needs_redraw - for mob_serialization in serialized_mobject_list: - mob_proto = response.mobjects.add() - mob_proto.id = mob_serialization["id"] - mob_proto.needs_redraw = mob_serialization["needs_redraw"] - for point in mob_serialization["points"]: - point_proto = mob_proto.points.add() + for point in mobject.points: + point_proto = mob_proto.vectorized_mobject_data.points.add() point_proto.x = point[0] point_proto.y = point[1] point_proto.z = point[2] - mob_proto.style.fill_color = mob_serialization["style"]["fill_color"] - mob_proto.style.fill_opacity = float(mob_serialization["style"]["fill_opacity"]) - mob_proto.style.stroke_color = mob_serialization["style"]["stroke_color"] - mob_proto.style.stroke_opacity = float( - mob_serialization["style"]["stroke_opacity"] - ) - mob_proto.style.stroke_width = float(mob_serialization["style"]["stroke_width"]) - return response - - -class UpdateFrontendHandler(FileSystemEventHandler): - """Logs all the events captured.""" - - def __init__(self, frame_server): - super().__init__() - self.frame_server = frame_server - - def on_moved(self, event): - super().on_moved(event) - raise NotImplementedError("Update not implemented for moved files.") - def on_deleted(self, event): - super().on_deleted(event) - raise NotImplementedError("Update not implemented for deleted files.") - - def on_modified(self, event): - super().on_modified(event) - module = get_module(config["input_file"]) - all_scene_classes = get_scene_classes_from_module(module) - scene_classes_to_render = get_scenes_to_render(all_scene_classes) - scene_class = scene_classes_to_render[0] - - # Get the old thread's ID. - old_thread_id = None - old_thread = self.frame_server.scene_thread - if hasattr(old_thread, "_thread_id"): - old_thread_id = old_thread._thread_id - if old_thread_id is None: - for thread_id, thread in threading._active.items(): - if thread is old_thread: - old_thread_id = thread_id - - # Stop the old thread. - res = ctypes.pythonapi.PyThreadState_SetAsyncExc( - old_thread_id, ctypes.py_object(SystemExit) - ) - if res > 1: - ctypes.pythonapi.PyThreadState_SetAsyncExc(old_thread_id, 0) - print("Exception raise failure") - old_thread.join() - - # Start a new thread. - self.frame_server.initialize_scene(scene_class, start_animation=1) - self.frame_server.scene.reached_start_animation.wait() - - # Serialize data on Animations up to the target one. - animations = [] - for scene in self.frame_server.keyframes: - if scene.animations: - animation_duration = scene.run_time - if len(scene.animations) == 1: - animation_name = str(scene.animations[0]) - else: - animation_name = f"{str(scene.animations[0])}..." - else: - animation_duration = scene.duration - animation_name = "Wait" - animations.append( - renderserver_pb2.Animation( - name=animation_name, - duration=animation_duration, - ) - ) + mob_style = mobject.get_style(simple=True) + mob_proto.style.fill_color = mob_style["fill_color"] + mob_proto.style.fill_opacity = float(mob_style["fill_opacity"]) + mob_proto.style.stroke_color = mob_style["stroke_color"] + mob_proto.style.stroke_opacity = float(mob_style["stroke_opacity"]) + mob_proto.style.stroke_width = float(mob_style["stroke_width"]) - # Reset the renderer. - with grpc.insecure_channel("localhost:50052") as channel: - stub = renderserver_pb2_grpc.RenderServerStub(channel) - request = renderserver_pb2.ManimStatusRequest( - scene_name=str(self.frame_server.scene), animations=animations + mob_proto.id = id(mobject) + elif isinstance(mobject, ImageMobject): + mob_proto.type = frameserver_pb2.MobjectData.MobjectType.IMAGE_MOBJECT + mob_style = mobject.get_style() + mob_proto.style.fill_color = mob_style["fill_color"] + mob_proto.style.fill_opacity = float(mob_style["fill_opacity"]) + assets_dir_path = str(config.get_dir("assets_dir")) + if mobject.path.startswith(assets_dir_path): + mob_proto.image_mobject_data.path = mobject.path[len(assets_dir_path) + 1 :] + else: + logger.info( + f"Expected path {mobject.path} to be under the assets dir ({assets_dir_path})" ) - try: - stub.ManimStatus(request) - except grpc._channel._InactiveRpcError: - sp.Popen(config["js_renderer_path"]) + mob_proto.image_mobject_data.height = mobject.get_height() + mob_proto.image_mobject_data.width = mobject.get_width() + mob_center = mobject.get_center() + mob_proto.image_mobject_data.center.x = mob_center[0] + mob_proto.image_mobject_data.center.y = mob_center[1] + mob_proto.image_mobject_data.center.z = mob_center[2] + return mob_proto -def get(scene_class): +def get(input_file_path): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) frameserver_pb2_grpc.add_FrameServerServicer_to_server( - FrameServer(server, scene_class), server + FrameServer(server, input_file_path), server ) server.add_insecure_port("localhost:50051") return server diff --git a/manim/grpc/proto/frameserver.proto b/manim/grpc/proto/frameserver.proto index 4c95e3b781..15ef5a892b 100644 --- a/manim/grpc/proto/frameserver.proto +++ b/manim/grpc/proto/frameserver.proto @@ -3,19 +3,40 @@ syntax = "proto3"; package frameserver; service FrameServer { - // Updates the scene to the specified animation offset and returns a - // serialization of the frame at that time. + // Returns a serialization of the scene at the specified time. rpc GetFrameAtTime (FrameRequest) returns (FrameResponse); - // Used to signal to the renderer that manim is running. - rpc RendererStatus (RendererStatusRequest) returns (RendererStatusResponse); + // Returns a list of the names and durations of all animations in the scene. + rpc FetchSceneData (EmptyRequest) returns (FetchSceneDataResponse); +} + +message FetchSceneDataResponse { + Scene scene = 1; +} - rpc UpdateSceneLocation (SceneLocationRequest) returns (SceneLocationResponse); +message Scene { + string name = 1; + repeated Animation animations = 2; + string background_color = 3; +} + +message Animation { + string name = 1; + float duration = 2; } message FrameRequest { - int64 animation_index = 1; - float animation_offset = 2; + float time_offset = 1; + int32 start_index = 2; + int32 end_index = 3; + int32 image_index = 4; + + enum PreviewMode { + ALL = 0; + ANIMATION_RANGE = 1; + IMAGE = 2; + } + PreviewMode preview_mode = 5; } message Style { @@ -34,9 +55,26 @@ message Point { message MobjectData { int64 id = 1; - repeated Point points = 2; - Style style = 3; - bool needs_redraw = 4; + Style style = 2; + enum MobjectType { + VMOBJECT = 0; + IMAGE_MOBJECT = 1; + } + MobjectType type = 3; + VMobjectData vectorized_mobject_data = 4; + ImageMobjectData image_mobject_data = 5; +} + +message VMobjectData { + repeated Point points = 1; + bool needs_redraw = 2; +} + +message ImageMobjectData { + string path = 1; + float height = 2; + float width = 3; + Point center = 4; } message FrameResponse { @@ -45,13 +83,10 @@ message FrameResponse { bool animation_finished = 3; bool scene_finished = 4; float duration = 5; - string animation_name = 6; -} - -message RendererStatusRequest {} -message RendererStatusResponse { - string scene_name = 1; + repeated string animations = 6; + int32 animation_index = 7; + float animation_offset = 8; } -message SceneLocationRequest {} -message SceneLocationResponse {} +message EmptyRequest {} +message EmptyResponse {} diff --git a/manim/grpc/proto/renderserver.proto b/manim/grpc/proto/renderserver.proto index c868ae2c3a..0d42c5244a 100644 --- a/manim/grpc/proto/renderserver.proto +++ b/manim/grpc/proto/renderserver.proto @@ -3,49 +3,26 @@ syntax = "proto3"; package renderserver; service RenderServer { - // Used to signal that a new animation is ready to be animated. - rpc AnimationStatus (EmptyRequest) returns (EmptyResponse); - - // Used to signal when manim starts and stops. - rpc ManimStatus (ManimStatusRequest) returns (ManimStatusResponse); - - rpc UpdateScene (UpdateSceneRequest) returns (UpdateSceneResponse); - - rpc NewScene (NewSceneRequest) returns (EmptyResponse); - - rpc SceneFinished (EmptyRequest) returns (EmptyResponse); + // Called from Manim when a scene has been newly rendered. + rpc UpdateSceneData (UpdateSceneDataRequest) returns (EmptyResponse); } -message EmptyRequest {} -message EmptyResponse {} +message UpdateSceneDataRequest { + Scene scene = 1; + string exception = 2; + bool has_exception = 3; +} -message NewSceneRequest { +message Scene { string name = 1; + repeated Animation animations = 2; + string background_color = 3; } -message NewSceneResponse {} - message Animation { string name = 1; float duration = 2; } -message AnimationStatusRequest {} -message AnimationStatusResponse {} - -message ManimStatusRequest { - // The name of the scene whose frames are currently being served. This is - // only seen if a javascript renderer is running prior to manim being - // started. - string scene_name = 1; - - // Indicates that the current Scene is finished. - bool scene_finished = 2; - - // Indicates that the Animations have been updated and should be changed. - repeated Animation animations = 3; -} -message ManimStatusResponse {} - -message UpdateSceneRequest {} -message UpdateSceneResponse {} +message EmptyRequest {} +message EmptyResponse {} diff --git a/manim/mobject/changing.py b/manim/mobject/changing.py index 88a705156f..74dd93fd4f 100644 --- a/manim/mobject/changing.py +++ b/manim/mobject/changing.py @@ -104,7 +104,7 @@ def construct(self): trace = TracedPath(circ.get_start) rolling_circle.add_updater(lambda m: m.rotate(-0.3)) self.add(trace, rolling_circle) - self.play(rolling_circle.shift, 8*RIGHT, run_time=4, rate_func=linear) + self.play(rolling_circle.animate.shift(8*RIGHT), run_time=4, rate_func=linear) """ diff --git a/manim/mobject/geometry.py b/manim/mobject/geometry.py index c3cf17f240..a33b38bf06 100644 --- a/manim/mobject/geometry.py +++ b/manim/mobject/geometry.py @@ -134,7 +134,7 @@ def add_tip(self, tip=None, tip_shape=None, tip_length=None, at_start=False): def create_tip(self, tip_shape=None, tip_length=None, at_start=False): """ - Stylises the tip, positions it spacially, and returns + Stylises the tip, positions it spatially, and returns the newly instantiated tip to the caller. """ tip = self.get_unpositioned_tip(tip_shape, tip_length) @@ -298,7 +298,7 @@ def set_pre_positioned_points(self): # Appropriate tangent lines to the circle d_theta = self.angle / (self.num_components - 1.0) tangent_vectors = np.zeros(anchors.shape) - # Rotate all 90 degress, via (x, y) -> (-y, x) + # Rotate all 90 degrees, via (x, y) -> (-y, x) tangent_vectors[:, 1] = anchors[:, 0] tangent_vectors[:, 0] = -anchors[:, 1] # Use tangent vectors to deduce anchors @@ -407,7 +407,7 @@ def __init__( def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2): # Ignores dim_to_match and stretch; result will always be a circle - # TODO: Perhaps create an ellipse class to handle singele-dimension stretching + # TODO: Perhaps create an ellipse class to handle single-dimension stretching # Something goes wrong here when surrounding lines? # TODO: Figure out and fix @@ -710,6 +710,20 @@ def set_opacity(self, opacity, family=True): class DashedLine(Line): + """A dashed Line. + + Examples + -------- + .. manim:: DashedLineExample + :save_last_frame: + + class DashedLineExample(Scene): + def construct(self): + dashed_line = DashedLine(config.frame_width/2*LEFT, 4*RIGHT) + self.add(dashed_line) + + """ + def __init__( self, *args, @@ -802,12 +816,12 @@ def __init__( self.preserve_tip_size_when_scaling = ( preserve_tip_size_when_scaling # is this used anywhere ) - tipp_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip) + tip_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip) Line.__init__(self, *args, buff=buff, stroke_width=stroke_width, **kwargs) # TODO, should this be affected when # Arrow.set_stroke is called? self.initial_stroke_width = self.stroke_width - self.add_tip(tip_shape=tipp_shape) + self.add_tip(tip_shape=tip_shape) self.set_stroke_width_from_length() def scale(self, factor, scale_tips=False, **kwargs): diff --git a/manim/mobject/graph.py b/manim/mobject/graph.py new file mode 100644 index 0000000000..8f705426e3 --- /dev/null +++ b/manim/mobject/graph.py @@ -0,0 +1,305 @@ +"""Mobjects used to represent mathematical graphs (think graph theory, not plotting).""" + +__all__ = [ + "Graph", +] + +from ..constants import UP +from ..utils.color import BLACK +from .types.vectorized_mobject import VMobject +from .geometry import Dot, Line, LabeledDot +from .svg.tex_mobject import MathTex + +from typing import Hashable, Union, List, Tuple + +from copy import copy +import networkx as nx +import numpy as np + + +class Graph(VMobject): + """An undirected graph (that is, a collection of vertices connected with edges). + + Graphs can be instantiated by passing both a list of (distinct, hashable) + vertex names, together with list of edges (as tuples of vertex names). See + the examples below for details. + + .. note:: + + This implementation uses updaters to make the edges move with + the vertices. + + Parameters + ---------- + + vertices + A list of vertices. Must be hashable elements. + edges + A list of edges, specified as tuples ``(u, v)`` where both ``u`` + and ``v`` are vertices. + labels + Controls whether or not vertices are labeled. If ``False`` (the default), + the vertices are not labeled; if ``True`` they are labeled using their + names (as specified in ``vertices``) via :class:`~.MathTex`. Alternatively, + custom labels can be specified by passing a dictionary whose keys are + the vertices, and whose values are the corresponding vertex labels + (rendered via, e.g., :class:`~.Text` or :class:`~.Tex`). + label_fill_color + Sets the fill color of the default labels generated when ``labels`` + is set to ``True``. Has no effect for other values of ``labels``. + layout + Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``, + ``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, and ``"spiral"`` + for automatic vertex positioning using ``networkx`` + (see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_ + for more details), or a dictionary specifying a coordinate (value) + for each vertex (key) for manual positioning. + layout_scale + The scale of automatically generated layouts: the vertices will + be arranged such that the coordinates are located within the + interval ``[-scale, scale]``. Default: 2. + layout_config + Only for automatically generated layouts. A dictionary whose entries + are passed as keyword arguments to the automatic layout algorithm + specified via ``layout`` of``networkx``. + vertex_type + The mobject class used for displaying vertices in the scene. + vertex_config + Either a dictionary containing keyword arguments to be passed to + the class specified via ``vertex_type``, or a dictionary whose keys + are the vertices, and whose values are dictionaries containing keyword + arguments for the mobject related to the corresponding vertex. + edge_type + The mobject class used for displaying edges in the scene. + edge_config + Either a dictionary containing keyword arguments to be passed + to the class specified via ``edge_type``, or a dictionary whose + keys are the edges, and whose values are dictionaries containing + keyword arguments for the mobject related to the corresponding edge. + + Examples + -------- + + First, we create a small graph and demonstrate that the edges move + together with the vertices. + + .. manim:: MovingVertices + + class MovingVertices(Scene): + def construct(self): + vertices = [1, 2, 3, 4] + edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)] + g = Graph(vertices, edges) + self.play(ShowCreation(g)) + self.wait() + self.play(g[1].animate.move_to([1, 1, 0]), + g[2].animate.move_to([-1, 1, 0]), + g[3].animate.move_to([1, -1, 0]), + g[4].animate.move_to([-1, -1, 0])) + self.wait() + + There are several automatic positioning algorithms to choose from: + + .. manim:: GraphAutoPosition + :save_last_frame: + + class GraphAutoPosition(Scene): + def construct(self): + vertices = [1, 2, 3, 4, 5, 6, 7, 8] + edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5), + (2, 8), (3, 4), (6, 1), (6, 2), + (6, 3), (7, 2), (7, 4)] + autolayouts = ["spring", "circular", "kamada_kawai", + "planar", "random", "shell", + "spectral", "spiral"] + graphs = [Graph(vertices, edges, layout=lt).scale(0.5) + for lt in autolayouts] + r1 = VGroup(*graphs[:3]).arrange() + r2 = VGroup(*graphs[3:6]).arrange() + r3 = VGroup(*graphs[6:]).arrange() + self.add(VGroup(r1, r2, r3).arrange(direction=DOWN)) + + Vertices can also be positioned manually: + + .. manim:: GraphManualPosition + :save_last_frame: + + class GraphManualPosition(Scene): + def construct(self): + vertices = [1, 2, 3, 4] + edges = [(1, 2), (2, 3), (3, 4), (4, 1)] + lt = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]} + G = Graph(vertices, edges, layout=lt) + self.add(G) + + The vertices in graphs can be labeled, and configurations for vertices + and edges can be modified both by default and for specific vertices and + edges. + + .. note:: + + In ``edge_config``, edges can be passed in both directions: if + ``(u, v)`` is an edge in the graph, both ``(u, v)`` as well + as ``(v, u)`` can be used as keys in the dictionary. + + .. manim:: LabeledModifiedGraph + :save_last_frame: + + class LabeledModifiedGraph(Scene): + def construct(self): + vertices = [1, 2, 3, 4, 5, 6, 7, 8] + edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5), + (2, 8), (3, 4), (6, 1), (6, 2), + (6, 3), (7, 2), (7, 4)] + g = Graph(vertices, edges, layout="circular", layout_scale=3, + labels=True, vertex_config={7: {"fill_color": RED}}, + edge_config={(1, 7): {"stroke_color": RED}, + (2, 7): {"stroke_color": RED}, + (4, 7): {"stroke_color": RED}}) + self.add(g) + + """ + + def __init__( + self, + vertices: List[Hashable], + edges: List[Tuple[Hashable, Hashable]], + labels: bool = False, + label_fill_color: str = BLACK, + layout: Union[str, dict] = "spring", + layout_scale: float = 2, + layout_config: Union[dict, None] = None, + vertex_type: "Mobject" = Dot, + vertex_config: Union[dict, None] = None, + edge_type: "Mobject" = Line, + edge_config: Union[dict, None] = None, + ) -> None: + VMobject.__init__(self) + + nx_graph = nx.Graph() + nx_graph.add_nodes_from(vertices) + nx_graph.add_edges_from(edges) + self._graph = nx_graph + + automatic_layouts = { + "circular": nx.layout.circular_layout, + "kamada_kawai": nx.layout.kamada_kawai_layout, + "planar": nx.layout.planar_layout, + "random": nx.layout.random_layout, + "shell": nx.layout.shell_layout, + "spectral": nx.layout.spectral_layout, + "spiral": nx.layout.spiral_layout, + "spring": nx.layout.spring_layout, + } + + if layout_config is None: + layout_config = {} + + if isinstance(layout, dict): + self._layout = layout + elif layout in automatic_layouts and layout != "random": + self._layout = automatic_layouts[layout]( + nx_graph, scale=layout_scale, **layout_config + ) + self._layout = dict( + [(k, np.append(v, [0])) for k, v in self._layout.items()] + ) + elif layout == "random": + # the random layout places coordinates in [0, 1) + # we need to rescale manually afterwards... + self._layout = automatic_layouts["random"](nx_graph, **layout_config) + for k, v in self._layout.items(): + self._layout[k] = 2 * layout_scale * (v - np.array([0.5, 0.5])) + self._layout = dict( + [(k, np.append(v, [0])) for k, v in self._layout.items()] + ) + else: + raise ValueError( + f"The layout '{layout}' is neither a recognized automatic layout, " + "nor a vertex placement dictionary." + ) + + if isinstance(labels, dict): + self._labels = labels + elif isinstance(labels, bool): + if labels: + self._labels = dict( + [(v, MathTex(v, fill_color=label_fill_color)) for v in vertices] + ) + else: + self._labels = dict() + + if self._labels and vertex_type is Dot: + vertex_type = LabeledDot + + # build vertex_config + if vertex_config is None: + vertex_config = {} + default_vertex_config = {} + if vertex_config: + default_vertex_config = dict( + [(k, v) for k, v in vertex_config.items() if k not in vertices] + ) + self._vertex_config = dict( + [(v, vertex_config.get(v, copy(default_vertex_config))) for v in vertices] + ) + for v, label in self._labels.items(): + self._vertex_config[v]["label"] = label + + self.vertices = dict( + [(v, vertex_type(**self._vertex_config[v])) for v in vertices] + ) + for v in self.vertices: + self[v].move_to(self._layout[v]) + + # build edge_config + if edge_config is None: + edge_config = {} + default_edge_config = {} + if edge_config: + default_edge_config = dict( + (k, v) + for k, v in edge_config.items() + if k not in edges and k[::-1] not in edges + ) + self._edge_config = {} + for e in edges: + if e in edge_config: + self._edge_config[e] = edge_config[e] + elif e[::-1] in edge_config: + self._edge_config[e] = edge_config[e[::-1]] + else: + self._edge_config[e] = copy(default_edge_config) + + self.edges = dict( + [ + ( + (u, v), + edge_type( + self[u].get_center(), + self[v].get_center(), + z_index=-1, + **self._edge_config[(u, v)], + ), + ) + for (u, v) in edges + ] + ) + + self.add(*self.vertices.values()) + self.add(*self.edges.values()) + + for (u, v), edge in self.edges.items(): + + def update_edge(e, u=u, v=v): + e.set_start_and_end_attrs(self[u].get_center(), self[v].get_center()) + e.generate_points() + + update_edge(edge) + edge.add_updater(update_edge) + + def __getitem__(self: "Graph", v: Hashable) -> "Mobject": + return self.vertices[v] + + def __repr__(self: "Graph") -> str: + return f"Graph on {len(self.vertices)} vertices and {len(self.edges)} edges" diff --git a/manim/mobject/matrix.py b/manim/mobject/matrix.py index eba1895819..8c3ffd238e 100644 --- a/manim/mobject/matrix.py +++ b/manim/mobject/matrix.py @@ -81,7 +81,7 @@ def __init__( **kwargs ): """ - Matrix can either either include numbres, tex_strings, + Matrix can either either include numbers, tex_strings, or mobjects """ self.v_buff = v_buff diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 17636e5373..a5fccd442c 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -57,6 +57,51 @@ def __init__(self, color=WHITE, name=None, dim=3, target=None, z_index=0, **kwar self.init_colors() Container.__init__(self, **kwargs) + @property + def animate(self): + """Used to animate the application of a method. + + .. warning:: + + Passing multiple animations for the same :class:`~.Mobject` in one + call to :meth:`~.Scene.play` is discouraged and will most likely + not work properly. Instead of writing an animation like + + :: + + self.play(my_mobject.animate.shift(RIGHT), my_mobject.animate.rotate(PI)) + + make use of method chaining for ``animate``, meaning:: + + self.play(my_mobject.animate.shift(RIGHT).rotate(PI)) + + Examples + -------- + + .. manim:: AnimateExample + + class AnimateExample(Scene): + def construct(self): + s = Square() + self.play(ShowCreation(s)) + self.play(s.animate.shift(RIGHT)) + self.play(s.animate.scale(2)) + self.play(s.animate.rotate(PI / 2)) + self.play(Uncreate(s)) + + + .. manim:: AnimateChainExample + + class AnimateChainExample(Scene): + def construct(self): + s = Square() + self.play(ShowCreation(s)) + self.play(s.animate.shift(RIGHT).scale(2).rotate(PI / 2)) + self.play(Uncreate(s)) + + """ + return _AnimationBuilder(self) + def __repr__(self): return str(self.name) @@ -1238,7 +1283,7 @@ def become(self, mobject, copy_submobjects=True): class BecomeScene(Scene): def construct(self): - circ= Circle(fill_color=RED) + circ = Circle(fill_color=RED) square = Square(fill_color=BLUE) self.add(circ) self.wait(0.5) @@ -1254,9 +1299,10 @@ def construct(self): # Errors def throw_error_if_no_points(self): if self.has_no_points(): - message = "Cannot call Mobject.{} " + "for a Mobject with no points" caller_name = sys._getframe(1).f_code.co_name - raise Exception(message.format(caller_name)) + raise Exception( + f"Cannot call Mobject.{caller_name} for a Mobject with no points" + ) # About z-index def set_z_index(self, z_index_value): @@ -1294,3 +1340,24 @@ class Group(Mobject): def __init__(self, *mobjects, **kwargs): Mobject.__init__(self, **kwargs) self.add(*mobjects) + + +class _AnimationBuilder: + def __init__(self, mobject, generate_target=True): + self.mobject = mobject + if generate_target: + self.mobject.generate_target() + + def __getattr__(self, method_name): + method = getattr(self.mobject.target, method_name) + + def update_target(*method_args, **method_kwargs): + method(*method_args, **method_kwargs) + return _AnimationBuilder(self.mobject, generate_target=False) + + return update_target + + def build(self): + from ..animation.transform import _MethodAnimation + + return _MethodAnimation(self.mobject) diff --git a/manim/mobject/mobject_update_utils.py b/manim/mobject/mobject_update_utils.py index ff4d943685..79f62f766a 100644 --- a/manim/mobject/mobject_update_utils.py +++ b/manim/mobject/mobject_update_utils.py @@ -37,7 +37,7 @@ def always(method, *args, **kwargs): def f_always(method, *arg_generators, **kwargs): """ More functional version of always, where instead - of taking in args, it takes in functions which ouput + of taking in args, it takes in functions which output the relevant arguments. """ assert_is_mobject_method(method) @@ -80,7 +80,7 @@ def turn_animation_into_updater(animation, cycle=False, **kwargs): the interpolation and update functions of the animation If cycle is True, this repeats over and over. Otherwise, - the updater will be popped uplon completion + the updater will be popped upon completion """ mobject = animation.mobject animation.update_config(**kwargs) diff --git a/manim/mobject/numbers.py b/manim/mobject/numbers.py index beaffb1fe5..85a56431d8 100644 --- a/manim/mobject/numbers.py +++ b/manim/mobject/numbers.py @@ -31,8 +31,7 @@ def construct(self): decimal.add_updater(lambda d: d.set_value(square.get_center()[1])) self.add(square, decimal) self.play( - square.to_edge, - DOWN, + square.animate.to_edge(DOWN), rate_func=there_and_back, run_time=5, ) @@ -261,7 +260,7 @@ def construct(self): self.wait() var_tracker = on_screen_var.tracker var = 10.5 - self.play(var_tracker.set_value, var) + self.play(var_tracker.animate.set_value(var)) self.wait() int_var = 0 @@ -275,7 +274,7 @@ def construct(self): self.wait() var_tracker = on_screen_int_var.tracker var = 10.5 - self.play(var_tracker.set_value, var) + self.play(var_tracker.animate.set_value(var)) self.wait() # If you wish to have a somewhat more complicated label for your diff --git a/manim/mobject/shape_matchers.py b/manim/mobject/shape_matchers.py index 25ef29f47a..758465adbe 100644 --- a/manim/mobject/shape_matchers.py +++ b/manim/mobject/shape_matchers.py @@ -56,7 +56,7 @@ def set_style( fill_opacity=None, family=True, ): - # Unchangable style, except for fill_opacity + # Unchangeable style, except for fill_opacity VMobject.set_style( self, stroke_color=BLACK, diff --git a/manim/mobject/svg/code_mobject.py b/manim/mobject/svg/code_mobject.py index d33580983e..06c55e7e5e 100644 --- a/manim/mobject/svg/code_mobject.py +++ b/manim/mobject/svg/code_mobject.py @@ -153,14 +153,14 @@ def __init__( style="vim", language=None, generate_html_file=False, - **kwargs + **kwargs, ): VGroup.__init__( self, stroke_width=stroke_width, background_stroke_color=background_stroke_color, background_stroke_width=background_stroke_width, - **kwargs + **kwargs, ) self.tab_width = tab_width self.line_spacing = line_spacing @@ -191,11 +191,11 @@ def __init__( self.line_numbers.next_to(self.code, direction=LEFT, buff=self.line_no_buff) if self.background == "rectangle": if self.insert_line_no: - forground = VGroup(self.code, self.line_numbers) + foreground = VGroup(self.code, self.line_numbers) else: - forground = self.code + foreground = self.code rect = SurroundingRectangle( - forground, + foreground, buff=self.margin, color=self.background_color, fill_color=self.background_color, @@ -207,11 +207,11 @@ def __init__( self.background_mobject = VGroup(rect) else: if self.insert_line_no: - forground = VGroup(self.code, self.line_numbers) + foreground = VGroup(self.code, self.line_numbers) else: - forground = self.code - height = forground.get_height() + 0.1 * 3 + 2 * self.margin - width = forground.get_width() + 0.1 * 3 + 2 * self.margin + foreground = self.code + height = foreground.get_height() + 0.1 * 3 + 2 * self.margin + width = foreground.get_width() + 0.1 * 3 + 2 * self.margin rect = RoundedRectangle( corner_radius=self.corner_radius, @@ -234,8 +234,8 @@ def __init__( ) self.background_mobject = VGroup(rect, buttons) - x = (height - forground.get_height()) / 2 - 0.1 * 3 - self.background_mobject.shift(forground.get_center()) + x = (height - foreground.get_height()) / 2 - 0.1 * 3 + self.background_mobject.shift(foreground.get_center()) self.background_mobject.shift(UP * x) if self.insert_line_no: VGroup.__init__( @@ -247,7 +247,7 @@ def __init__( self.background_mobject, Dot(fill_opacity=0, stroke_opacity=0), self.code, - **kwargs + **kwargs, ) self.move_to(np.array([0, 0, 0])) @@ -263,8 +263,9 @@ def ensure_valid_file(self): if os.path.exists(path): self.file_path = path return - error = "From: {}, could not find {} at either of these locations: {}".format( - os.getcwd(), self.file_name, possible_paths + error = ( + f"From: {os.getcwd()}, could not find {self.file_name} at either " + + "of these locations: {possible_paths}" ) raise IOError(error) @@ -286,7 +287,7 @@ def gen_line_numbers(self): alignment="right", font=self.font, disable_ligatures=True, - stroke_width=self.stroke_width + stroke_width=self.stroke_width, ).scale(self.scale_factor) for i in line_numbers: i.set_color(self.default_color) @@ -312,7 +313,7 @@ def gen_colored_lines(self): tab_width=self.tab_width, font=self.font, disable_ligatures=True, - stroke_width=self.stroke_width + stroke_width=self.stroke_width, ).scale(self.scale_factor) for line_no in range(code.__len__()): line = code.chars[line_no] diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index b5cf62839e..1a5586d1c5 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -79,7 +79,7 @@ def __init__( unpack_groups=True, # if False, creates a hierarchy of VGroups stroke_width=DEFAULT_STROKE_WIDTH, fill_opacity=1.0, - **kwargs + **kwargs, ): self.file_name = file_name or self.file_name self.ensure_valid_file() @@ -120,9 +120,7 @@ def ensure_valid_file(self): if os.path.exists(path): self.file_path = path return - error = "From: {}, could not find {} at either of these locations: {}".format( - os.getcwd(), self.file_name, possible_paths - ) + error = f"From: {os.getcwd()}, could not find {self.file_name} at either of these locations: {possible_paths}" raise IOError(error) def generate_points(self): @@ -199,7 +197,7 @@ def g_to_mobjects(self, g_element): Returns ------- List[VMobject] - A list of VMobject reprsented by the group. + A list of VMobject represented by the group. """ mob = VGroup(*self.get_mobjects_from(g_element)) self.handle_transforms(g_element, mob) @@ -348,7 +346,7 @@ def rect_to_mobject(self, rect_element): WHITE ): opacity = 0 - fill_color = BLACK # shdn't be necessary but avoids error msgs + fill_color = BLACK # shouldn't be necessary but avoids error msgs if fill_color in ["#000", "#000000"]: fill_color = WHITE if stroke_color in ["", "none", "#FFF", "#FFFFFF"] or Color( @@ -390,7 +388,7 @@ def rect_to_mobject(self, rect_element): return mob def handle_transforms(self, element, mobject): - """Applies the SVG transform to the specified mobject. Tranforms include: + """Applies the SVG transform to the specified mobject. Transforms include: ``rotate``, ``translate``, ``scale``, and ``skew``. Parameters diff --git a/manim/mobject/svg/tex_mobject.py b/manim/mobject/svg/tex_mobject.py index 7eb65746f4..a8364f9e95 100644 --- a/manim/mobject/svg/tex_mobject.py +++ b/manim/mobject/svg/tex_mobject.py @@ -263,7 +263,7 @@ def modify_special_strings(self, tex): # Fraction line needs something to be over tex == "\\over", tex == "\\overline", - # Makesure sqrt has overbar + # Make sure sqrt has overbar tex == "\\sqrt", # Need to add blank subscript or superscript tex.endswith("_"), @@ -329,7 +329,7 @@ def get_tex_string(self): def path_string_to_mobject(self, path_string): # Overwrite superclass default to use # specialized path_string mobject - return TexSymbol(path_string) + return TexSymbol(path_string, z_index=self.z_index) def organize_submobjects_left_to_right(self): self.sort(lambda p: p[0]) @@ -406,7 +406,7 @@ def break_up_tex_strings(self, tex_strings): def break_up_by_substrings(self): """ - Reorganize existing submojects one layer + Reorganize existing submobjects one layer deeper based on the structure of tex_strings (as a list of tex_strings) """ @@ -417,11 +417,12 @@ def break_up_by_substrings(self): tex_string, tex_environment=self.tex_environment, tex_template=self.tex_template, + z_index=self.z_index, ) num_submobs = len(sub_tex_mob.submobjects) new_index = curr_index + num_submobs if num_submobs == 0: - # For cases like empty tex_strings, we want the corresponing + # For cases like empty tex_strings, we want the corresponding # part of the whole MathTex to be a VectorizedPoint # positioned in the right part of the MathTex sub_tex_mob.submobjects = [VectorizedPoint()] @@ -534,7 +535,7 @@ def fade_all_but(self, index_or_string, opacity=0.5): elif isinstance(arg, int): part = self.submobjects[arg] else: - raise TypeError("Expected int or string, got {0}".format(arg)) + raise TypeError(f"Expected int or string, got {arg}") for other_part in self.submobjects: if other_part is part: other_part.set_fill(opacity=1) diff --git a/manim/mobject/svg/text_mobject.py b/manim/mobject/svg/text_mobject.py index b49c71a128..ac14583a7d 100644 --- a/manim/mobject/svg/text_mobject.py +++ b/manim/mobject/svg/text_mobject.py @@ -14,10 +14,10 @@ def construct(self): text = Text('Hello world').scale(3) self.add(text) -.. manim:: TextAlignement +.. manim:: TextAlignment :save_last_frame: - class TextAlignement(Scene): + class TextAlignment(Scene): def construct(self): title = Text("K-means clustering and Logistic Regression", color=WHITE) title.scale_in_place(0.75) @@ -42,7 +42,7 @@ def construct(self): """ -__all__ = ["Text", "Paragraph", "CairoText"] +__all__ = ["Text", "Paragraph", "CairoText", "MarkupText"] import copy @@ -53,16 +53,15 @@ def construct(self): from xml.sax.saxutils import escape import cairo -import cairocffi -import pangocairocffi -import pangocffi +import manimpango +from manimpango import PangoUtils, TextSetting, MarkupUtils from ... import config, logger from ...constants import * from ...mobject.geometry import Dot from ...mobject.svg.svg_mobject import SVGMobject from ...mobject.types.vectorized_mobject import VGroup -from ...utils.color import WHITE +from ...utils.color import WHITE, Colors TEXT_MOB_SCALE_FACTOR = 0.05 @@ -101,16 +100,6 @@ def remove_invisible_chars(mobject): return mobject_without_dots -class TextSetting(object): - def __init__(self, start, end, font, slant, weight, line_num=-1): - self.start = start - self.end = end - self.font = font - self.slant = slant - self.weight = weight - self.line_num = line_num - - class CairoText(SVGMobject): """Display (non-LaTeX) text. @@ -118,13 +107,6 @@ class CairoText(SVGMobject): in the given text. In particular, slicing is possible. - - .. WARNING:: - - Using a :class:`.Transform` on text with leading whitespace can look - `weird <https://github.com/3b1b/manim/issues/1067>`_. Consider using - :meth:`remove_invisible_chars` to resolve this issue. - Tests ----- @@ -151,8 +133,8 @@ def __init__( gradient=None, line_spacing=-1, size=1, - slant=NORMAL, - weight=NORMAL, + slant: str = NORMAL, + weight: str = NORMAL, t2c=None, t2f=None, t2g=None, @@ -200,7 +182,7 @@ def __init__( else: self.line_spacing = self.size + self.size * self.line_spacing file_name = self.text2svg() - self.remove_last_M(file_name) + PangoUtils.remove_last_M(file_name) SVGMobject.__init__( self, file_name, @@ -256,7 +238,7 @@ def gen_chars(self): or self.text[char_index] == "\t" or self.text[char_index] == "\n" ): - space = Dot(redius=0, fill_opacity=0, stroke_opacity=0) + space = Dot(radius=0, fill_opacity=0, stroke_opacity=0) if char_index == 0: space.move_to(self.submobjects[submobjects_char_index].get_center()) else: @@ -269,13 +251,6 @@ def gen_chars(self): submobjects_char_index += 1 return chars - def remove_last_M(self, file_name): - with open(file_name, "r") as fpr: - content = fpr.read() - content = re.sub(r'Z M [^A-Za-z]*? "\/>', 'Z "/>', content) - with open(file_name, "w") as fpw: - fpw.write(content) - def find_indexes(self, word, text): m = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word) if m: @@ -408,7 +383,7 @@ def text2svg(self): offset_x = 0 last_line_num = 0 for setting in settings: - font = setting.font + font = setting.font.decode("utf-8") slant = self.str2slant(setting.slant) weight = self.str2weight(setting.weight) text = self.text[setting.start : setting.end].replace("\n", " ") @@ -433,21 +408,11 @@ class Paragraph(VGroup): :class:`.VGroup` containing all the lines. In this context, every line is constructed as a :class:`.VGroup` of characters contained in the line. - .. WARNING:: - - Using a :class:`.Transform` on text with leading whitespace can look - `weird <https://github.com/3b1b/manim/issues/1067>`_. Consider using - :meth:`remove_invisible_chars` to resolve this issue. - - .. note:: - - Due to issues with the Pango-powered :class:`.Text`, this class uses - :class:`.CairoText`. Parameters ---------- line_spacing : :class:`int`, optional - Represents the spaning betweeb lines. Default to -1, which means auto. + Represents the spacing between lines. Default to -1, which means auto. alignment : :class:`str`, optional Defines the alignment of paragraph. Default to "left". Possible values are "left", "right", "center" @@ -473,7 +438,7 @@ def __init__(self, *text, line_spacing=-1, alignment=None, **config): VGroup.__init__(self, **config) lines_str = "\n".join(list(text)) - self.lines_text = CairoText(lines_str, **config) + self.lines_text = Text(lines_str, **config) lines_str_list = lines_str.split("\n") self.chars = self.gen_chars(lines_str_list) @@ -534,7 +499,7 @@ def gen_chars(self, lines_str_list): return chars def set_all_lines_alignments(self, alignment): - """Function to set all line's aligment to a specific value. + """Function to set all line's alignment to a specific value. Parameters ---------- @@ -546,7 +511,7 @@ def set_all_lines_alignments(self, alignment): return self def set_line_alignment(self, alignment, line_no): - """Function to set one line's aligment to a specific value. + """Function to set one line's alignment to a specific value. Parameters ---------- @@ -580,7 +545,7 @@ def set_line_to_initial_position(self, line_no): return self def change_alignment_for_a_line(self, alignment, line_no): - """Function to change one line's aligment to a specific value. + """Function to change one line's alignment to a specific value. Parameters ---------- @@ -719,12 +684,6 @@ def construct(self): >>> Text('The horse does not eat cucumber salad.') Text('The horse does not eat cucumber salad.') - .. WARNING:: - - Using a :class:`.Transform` on text with leading whitespace can look - `weird <https://github.com/3b1b/manim/issues/1067>`_. Consider using - :meth:`remove_invisible_chars` to resolve this issue. - """ def __init__( @@ -736,8 +695,8 @@ def __init__( size: int = 1, line_spacing: int = -1, font: str = "", - slant=NORMAL, - weight=NORMAL, + slant: str = NORMAL, + weight: str = NORMAL, t2c: Dict[str, str] = None, t2f: Dict[str, str] = None, t2g: Dict[str, tuple] = None, @@ -798,7 +757,7 @@ def __init__( else: self.line_spacing = self.size + self.size * self.line_spacing file_name = self.text2svg() - self.remove_last_M(file_name) + PangoUtils.remove_last_M(file_name) SVGMobject.__init__( self, file_name, @@ -815,7 +774,6 @@ def __init__( if self.disable_ligatures: self.submobjects = [*self.gen_chars()] self.chars = VGroup(*self.submobjects) - self.chars = VGroup(*self.submobjects) self.text = text_without_tabs.replace(" ", "").replace("\n", "") nppc = self.n_points_per_cubic_curve for each in self: @@ -852,7 +810,7 @@ def gen_chars(self): submobjects_char_index = 0 for char_index in range(self.text.__len__()): if self.text[char_index] in (" ", "\t", "\n"): - space = Dot(redius=0, fill_opacity=0, stroke_opacity=0) + space = Dot(radius=0, fill_opacity=0, stroke_opacity=0) if char_index == 0: space.move_to(self.submobjects[submobjects_char_index].get_center()) else: @@ -865,14 +823,6 @@ def gen_chars(self): submobjects_char_index += 1 return chars - def remove_last_M(self, file_name: str): # pylint: disable=invalid-name - """Internally used. Use to format the rendered SVG files.""" - with open(file_name, "r") as fpr: - content = fpr.read() - content = re.sub(r'Z M [^A-Za-z]*? "\/>', 'Z "/>', content) - with open(file_name, "w") as fpw: - fpw.write(content) - def find_indexes(self, word: str, text: str): """Internally used function. Finds the indexes of ``text`` in ``word``.""" temp = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word) @@ -890,7 +840,7 @@ def find_indexes(self, word: str, text: str): return indexes # def full2short(self, kwargs): - # """Internally used function. Fomats some exapansion to short forms. + # """Internally used function. Formats some expansion to short forms. # text2color -> t2c # text2font -> t2f # text2gradient -> t2g @@ -923,46 +873,6 @@ def set_color_by_t2g(self, t2g=None): for start, end in self.find_indexes(word, self.original_text): self.chars[start:end].set_color_by_gradient(*gradient) - def str2style(self, string): - """Internally used function. Converts text to Pango Understandable Styles.""" - if string == NORMAL: - return pangocffi.Style.NORMAL - elif string == ITALIC: - return pangocffi.Style.ITALIC - elif string == OBLIQUE: - return pangocffi.Style.OBLIQUE - else: - raise AttributeError("There is no Style Called %s" % string) - - def str2weight(self, string): - """Internally used function. Convert text to Pango Understandable Weight""" - if string == NORMAL: - return pangocffi.Weight.NORMAL - elif string == BOLD: - return pangocffi.Weight.BOLD - elif string == THIN: - return pangocffi.Weight.THIN - elif string == ULTRALIGHT: - return pangocffi.Weight.ULTRALIGHT - elif string == LIGHT: - return pangocffi.Weight.LIGHT - elif string == SEMILIGHT: - return pangocffi.Weight.SEMILIGHT - elif string == BOOK: - return pangocffi.Weight.BOOK - elif string == MEDIUM: - return pangocffi.Weight.MEDIUM - elif string == SEMIBOLD: - return pangocffi.Weight.SEMIBOLD - elif string == ULTRABOLD: - return pangocffi.Weight.ULTRABOLD - elif string == HEAVY: - return pangocffi.Weight.HEAVY - elif string == ULTRAHEAVY: - return pangocffi.Weight.ULTRAHEAVY - else: - raise AttributeError("There is no Font Weight Called %s" % string) - def text2hash(self): """Internally used function. Generates ``sha256`` hash for file name. @@ -972,6 +882,7 @@ def text2hash(self): ) # to differentiate Text and CairoText settings += str(self.t2f) + str(self.t2s) + str(self.t2w) settings += str(self.line_spacing) + str(self.size) + settings += str(self.disable_ligatures) id_str = self.text + settings hasher = hashlib.sha256() hasher.update(id_str.encode()) @@ -1036,40 +947,436 @@ def text2svg(self): file_name = os.path.join(dir_name, hash_name) + ".svg" if os.path.exists(file_name): return file_name - surface = cairocffi.SVGSurface(file_name, 600, 400) - context = cairocffi.Context(surface) - context.move_to(START_X, START_Y) settings = self.text2settings() - offset_x = 0 - last_line_num = 0 - layout = pangocairocffi.create_layout(context) - layout.set_width(pangocffi.units_from_double(600)) - for setting in settings: - family = setting.font - style = self.str2style(setting.slant) - weight = self.str2weight(setting.weight) - text = self.text[setting.start : setting.end].replace("\n", " ") - fontdesc = pangocffi.FontDescription() - fontdesc.set_size(pangocffi.units_from_double(size)) - if family: - fontdesc.set_family(family) - fontdesc.set_style(style) - fontdesc.set_weight(weight) - layout.set_font_description(fontdesc) - if setting.line_num != last_line_num: - offset_x = 0 - last_line_num = setting.line_num - context.move_to( - START_X + offset_x, START_Y + line_spacing * setting.line_num + width = 600 + height = 400 + + return manimpango.text2svg( + settings, + size, + line_spacing, + disable_liga, + file_name, + START_X, + START_Y, + width, + height, + self.text, + ) + + +class MarkupText(SVGMobject): + r"""Display (non-LaTeX) text rendered using `Pango <https://pango.gnome.org/>`_. + + Text objects behave like a :class:`.VGroup`-like iterable of all characters + in the given text. In particular, slicing is possible. Text can be formatted + using different tags: + + - ``<b>bold</b>``, ``<i>italic</i>`` and ``<b><i>bold+italic</i></b>`` + - ``<ul>underline</ul>`` and ``<s>strike through</s>`` + - ``<tt>typewriter font</tt>`` + - ``<big>bigger font</big>`` and ``<small>smaller font</small>`` + - ``<sup>superscript</sup>`` and ``<sub>subscript</sub>`` + - ``<span underline="double">double underline</span>`` + - ``<span underline="error">error underline</span>`` + - ``<span font_family="sans">temporary change of font</span>`` + - ``<color col="RED">temporary change of color</color>``; colors can be specified as Manim constants like ``RED`` or ``YELLOW`` or as hex triples like ``#aabbaa`` + - ``<gradient from="YELLOW" to="RED">temporary gradient</gradient>``; colors specified as above + + If your text contains ligatures, the :class:`MarkupText` class may incorrectly determine + the first and last letter to be colored. This is due to the fact that e.g. ``fl`` + are two characters, but might be set as one single glyph, a ligature. If your language does + not depend on ligatures, consider setting ``disable_ligatures=True``. If you cannot + or do not want to do without ligatures, the ``gradient`` and ``color`` tag support + an optional attribute ``offset`` which can be used to compensate for that error. + Usage is as follows: + + - ``<color col="RED" offset="1">red text</color>`` to *start* coloring one letter earlier + - ``<color col="RED" offset=",1">red text</color>`` to *end* coloring one letter earlier + - ``<color col="RED" offset="2,1">red text</color>`` to *start* coloring two letters earlier and *end* one letter earlier + + Specifying a second offset may be necessary if the text to be colored does + itself contain ligatures. The same can happen when using HTML entities for + special chars. + + Escaping of special characters: ``>`` *should* be written as ``>`` whereas ``<`` and + ``&`` *must* be written as ``<`` and ``&``. + + You can find more information about Pango markup formatting at the + corresponding documentation page: + `Pango Markup <https://developer.gnome.org/pango/stable/pango-Markup.html>`_. + Please be aware that not all features are supported by this class and that + the ``<color>`` and ``<gradient>`` tags mentioned above are not supported by Pango. + + Parameters + ---------- + text : :class:`str` + The text that need to created as mobject. + fill_opacity : :class:`int` + The fill opacity with 1 meaning opaque and 0 meaning transparent. + stroke_width : :class:`int` + Stroke width. + color : :class:`str` + Global color setting for the entire text. Local overrides are possible. + size : :class:`int` + Font size. + line_spacing : :class:`int` + Line spacing. + font : :class:`str` + Global font setting for the entire text. Local overrides are possible. + slant : :class:`str` + Global slant setting, e.g. `NORMAL` or `ITALIC`. Local overrides are possible. + weight : :class:`str` + Global weight setting, e.g. `NORMAL` or `BOLD`. Local overrides are possible. + gradient: :class:`tuple` + Global gradient setting. Local overrides are possible. + + + Returns + ------- + :class:`MarkupText` + The text displayed in form of a :class:`.VGroup`-like mobject. + + Examples + --------- + + .. manim:: BasicMarkupExample + :save_last_frame: + + class BasicMarkupExample(Scene): + def construct(self): + text1 = MarkupText("<b>foo</b> <i>bar</i> <b><i>foobar</i></b>") + text2 = MarkupText("<s>foo</s> <u>bar</u> <big>big</big> <small>small</small>") + text3 = MarkupText("H<sub>2</sub>O and H<sub>3</sub>O<sup>+</sup>") + text4 = MarkupText("type <tt>help</tt> for help") + text5 = MarkupText( + '<span underline="double">foo</span> <span underline="error">bar</span>' + ) + group = VGroup(text1, text2, text3, text4, text5).arrange(DOWN) + self.add(group) + + .. manim:: ColorExample + :save_last_frame: + + class ColorExample(Scene): + def construct(self): + text1 = MarkupText( + 'all in red <color col="YELLOW">except this</color>', color=RED + ) + text2 = MarkupText("nice gradient", gradient=(BLUE, GREEN)) + text3 = MarkupText( + 'nice <gradient from="RED" to="YELLOW">intermediate</gradient> gradient', + gradient=(BLUE, GREEN), + ) + text4 = MarkupText( + 'fl ligature <color col="#00ff00">causing trouble</color> here' + ) + text5 = MarkupText( + 'fl ligature <color col="#00ff00" offset="1">defeated</color> with offset' + ) + text6 = MarkupText( + 'fl ligature <color col="GREEN" offset="1">floating</color> inside' + ) + text7 = MarkupText( + 'fl ligature <color col="GREEN" offset="1,1">floating</color> inside' + ) + group = VGroup(text1, text2, text3, text4, text5, text6, text7).arrange(DOWN) + self.add(group) + + .. manim:: FontExample + :save_last_frame: + + class FontExample(Scene): + def construct(self): + text1 = MarkupText( + 'all in sans <span font_family="serif">except this</span>', font="sans" + ) + text2 = MarkupText( + '<span font_family="serif">mixing</span> <span font_family="sans">fonts</span> <span font_family="monospace">is ugly</span>' + ) + text3 = MarkupText("special char > or >") + text4 = MarkupText("special char < and &") + group = VGroup(text1, text2, text3, text4).arrange(DOWN) + self.add(group) + + .. manim:: NewlineExample + :save_last_frame: + + class NewlineExample(Scene): + def construct(self): + text = MarkupText('foooo<color col="RED">oo\nbaa</color>aar') + self.add(text) + + .. manim:: NoLigaturesExample + :save_last_frame: + + class NoLigaturesExample(Scene): + def construct(self): + text1 = MarkupText('fl<color col="RED">oat</color>ing') + text2 = MarkupText('fl<color col="RED">oat</color>ing', disable_ligatures=True) + group = VGroup(text1, text2).arrange(DOWN) + self.add(group) + + + As :class:`MarkupText` uses Pango to render text, rendering non-English + characters is easily possible: + + .. manim:: MultiLanguage + :save_last_frame: + + class MultiLanguage(Scene): + def construct(self): + morning = MarkupText("வணக்கம்", font="sans-serif") + chin = MarkupText( + '見 角 言 谷 辛 <color col="BLUE">辰 辵 邑</color> 酉 釆 里!' + ) # works as in ``Text``. + mess = MarkupText("Multi-Language", style=BOLD) + russ = MarkupText("Здравствуйте मस नम म ", font="sans-serif") + hin = MarkupText("नमस्ते", font="sans-serif") + japanese = MarkupText("臂猿「黛比」帶著孩子", font="sans-serif") + group = VGroup(morning, chin, mess, russ, hin, japanese).arrange(DOWN) + self.add(group) + + + Tests + ----- + + Check that the creation of :class:`~.MarkupText` works:: + + >>> MarkupText('The horse does not eat cucumber salad.') + MarkupText('The horse does not eat cucumber salad.') + + """ + + def __init__( + self, + text: str, + fill_opacity: int = 1, + stroke_width: int = 0, + color: str = WHITE, + size: int = 1, + line_spacing: int = -1, + font: str = "", + slant: str = NORMAL, + weight: str = NORMAL, + gradient: tuple = None, + tab_width: int = 4, + height: int = None, + width: int = None, + should_center: bool = True, + unpack_groups: bool = True, + disable_ligatures: bool = False, + **kwargs, + ): + self.text = text + self.size = size + self.line_spacing = line_spacing + self.font = font + self.slant = slant + self.weight = weight + self.gradient = gradient + self.tab_width = tab_width + + self.original_text = text + self.disable_ligatures = disable_ligatures + text_without_tabs = text + if "\t" in text: + text_without_tabs = text.replace("\t", " " * self.tab_width) + + colormap = self.extract_color_tags() + gradientmap = self.extract_gradient_tags() + + if self.line_spacing == -1: + self.line_spacing = self.size + self.size * 0.3 + else: + self.line_spacing = self.size + self.size * self.line_spacing + + file_name = self.text2svg() + PangoUtils.remove_last_M(file_name) + SVGMobject.__init__( + self, + file_name, + color=color, + fill_opacity=fill_opacity, + stroke_width=stroke_width, + height=height, + width=width, + should_center=should_center, + unpack_groups=unpack_groups, + **kwargs, + ) + self.chars = VGroup(*self.submobjects) + self.text = text_without_tabs.replace(" ", "").replace("\n", "") + + nppc = self.n_points_per_cubic_curve + for each in self: + if len(each.points) == 0: + continue + points = each.points + last = points[0] + each.clear_points() + for index, point in enumerate(points): + each.append_points([point]) + if ( + index != len(points) - 1 + and (index + 1) % nppc == 0 + and any(point != points[index + 1]) + ): + each.add_line_to(last) + last = points[index + 1] + each.add_line_to(last) + + if self.gradient: + self.set_color_by_gradient(*self.gradient) + for col in colormap: + self.chars[ + col["start"] + - col["start_offset"] : col["end"] + - col["start_offset"] + - col["end_offset"] + ].set_color(self._parse_color(col["color"])) + for grad in gradientmap: + self.chars[ + grad["start"] + - grad["start_offset"] : grad["end"] + - grad["start_offset"] + - grad["end_offset"] + ].set_color_by_gradient( + *(self._parse_color(grad["from"]), self._parse_color(grad["to"])) ) - pangocairocffi.update_layout(context, layout) - if disable_liga: - text = escape(text) - layout.set_markup(f"<span font_features='liga=0'>{text}</span>") - else: - layout.set_text(text) - logger.debug(f"Setting Text {text}") - pangocairocffi.show_layout(context, layout) - offset_x += pangocffi.units_to_double(layout.get_size()[0]) - surface.finish() - return file_name + # anti-aliasing + if self.height is None and self.width is None: + self.scale(TEXT_MOB_SCALE_FACTOR) + + def text2hash(self): + """Generates ``sha256`` hash for file name.""" + settings = ( + "MARKUPPANGO" + self.font + self.slant + self.weight + ) # to differentiate from classical Pango Text + settings += str(self.line_spacing) + str(self.size) + settings += str(self.disable_ligatures) + id_str = self.text + settings + hasher = hashlib.sha256() + hasher.update(id_str.encode()) + return hasher.hexdigest()[:16] + + def text2svg(self): + """Convert the text to SVG using Pango.""" + size = self.size * 10 + line_spacing = self.line_spacing * 10 + dir_name = config.get_dir("text_dir") + disable_liga = self.disable_ligatures + if not os.path.exists(dir_name): + os.makedirs(dir_name) + hash_name = self.text2hash() + file_name = os.path.join(dir_name, hash_name) + ".svg" + if os.path.exists(file_name): + return file_name + + logger.debug(f"Setting Text {self.text}") + return MarkupUtils.text2svg( + self.text, + self.font, + self.slant, + self.weight, + size, + line_spacing, + disable_liga, + file_name, + START_X, + START_Y, + 600, # width + 400, # height + ) + + def _count_real_chars(self, s): + """Counts characters that will be displayed. + + This is needed for partial coloring or gradients, because space + counts to the text's `len`, but has no corresponding character.""" + count = 0 + level = 0 + # temporarily replace HTML entities by single char + s = re.sub("&[^;]+;", "x", s) + for c in s: + if c == "<": + level += 1 + if c == ">" and level > 0: + level -= 1 + elif c != " " and c != "\t" and level == 0: + count += 1 + return count + + def extract_gradient_tags(self): + """Used to determine which parts (if any) of the string should be formatted + with a gradient. + + Removes the ``<gradient>`` tag, as it is not part of Pango's markup and would cause an error. + """ + tags = re.finditer( + '<gradient\s+from="([^"]+)"\s+to="([^"]+)"(\s+offset="([^"]+)")?>(.+?)</gradient>', + self.original_text, + re.S, + ) + gradientmap = [] + for tag in tags: + start = self._count_real_chars(self.original_text[: tag.start(0)]) + end = start + self._count_real_chars(tag.group(5)) + offsets = tag.group(4).split(",") if tag.group(4) else [0] + start_offset = int(offsets[0]) if offsets[0] else 0 + end_offset = int(offsets[1]) if len(offsets) == 2 and offsets[1] else 0 + + gradientmap.append( + { + "start": start, + "end": end, + "from": tag.group(1), + "to": tag.group(2), + "start_offset": start_offset, + "end_offset": end_offset, + } + ) + self.text = re.sub("<gradient[^>]+>(.+?)</gradient>", r"\1", self.text, 0, re.S) + return gradientmap + + def _parse_color(self, col): + """Parse color given in ``<color>`` or ``<gradient>`` tags.""" + if re.match("#[0-9a-f]{6}", col): + return col + else: + return Colors[col.lower()].value + + def extract_color_tags(self): + """Used to determine which parts (if any) of the string should be formatted + with a custom color. + + Removes the ``<color>`` tag, as it is not part of Pango's markup and would cause an error. + """ + tags = re.finditer( + '<color\s+col="([^"]+)"(\s+offset="([^"]+)")?>(.+?)</color>', + self.original_text, + re.S, + ) + + colormap = [] + for tag in tags: + start = self._count_real_chars(self.original_text[: tag.start(0)]) + end = start + self._count_real_chars(tag.group(4)) + offsets = tag.group(3).split(",") if tag.group(3) else [0] + start_offset = int(offsets[0]) if offsets[0] else 0 + end_offset = int(offsets[1]) if len(offsets) == 2 and offsets[1] else 0 + + colormap.append( + { + "start": start, + "end": end, + "color": tag.group(1), + "start_offset": start_offset, + "end_offset": end_offset, + } + ) + self.text = re.sub("<color[^>]+>(.+?)</color>", r"\1", self.text, 0, re.S) + return colormap + + def __repr__(self): + return f"MarkupText({repr(self.original_text)})" diff --git a/manim/mobject/three_dimensions.py b/manim/mobject/three_dimensions.py index 6c41acb9c4..f4c52dd563 100644 --- a/manim/mobject/three_dimensions.py +++ b/manim/mobject/three_dimensions.py @@ -114,6 +114,7 @@ def set_fill_by_checkerboard(self, *colors, opacity=None): for face in self: c_index = (face.u_index + face.v_index) % n_colors face.set_fill(colors[c_index], opacity=opacity) + return self # Specific shapes diff --git a/manim/mobject/types/image_mobject.py b/manim/mobject/types/image_mobject.py index 039f9285c4..8fdd6e5c8f 100644 --- a/manim/mobject/types/image_mobject.py +++ b/manim/mobject/types/image_mobject.py @@ -3,6 +3,7 @@ __all__ = ["AbstractImageMobject", "ImageMobject", "ImageMobjectFromCamera"] import pathlib +import colour import numpy as np @@ -38,7 +39,7 @@ def get_pixel_array(self): raise NotImplementedError() def set_color(self): - # Likely to be implemented in subclasses, but no obgligation + # Likely to be implemented in subclasses, but no obligation pass def reset_points(self): @@ -92,12 +93,15 @@ def __init__( image_mode="RGBA", **kwargs, ): + self.fill_opacity = 1 + self.stroke_opacity = 1 self.invert = invert self.image_mode = image_mode if isinstance(filename_or_array, (str, pathlib.PurePath)): path = get_full_raster_image_path(filename_or_array) image = Image.open(path).convert(self.image_mode) self.pixel_array = np.array(image) + self.path = path else: self.pixel_array = np.array(filename_or_array) self.pixel_array_dtype = kwargs.get("pixel_array_dtype", "uint8") @@ -144,6 +148,8 @@ def set_opacity(self, alpha): transparent. """ self.pixel_array[:, :, 3] = int(255 * alpha) + self.fill_opacity = alpha + self.stroke_opacity = alpha return self def fade(self, darkness=0.5, family=True): @@ -168,11 +174,11 @@ def interpolate_color(self, mobject1, mobject2, alpha): Parameters ---------- mobject1 : ImageMobject - The ImageMobject to tranform from. + The ImageMobject to transform from. mobject1 : ImageMobject - The ImageMobject to tranform into. + The ImageMobject to transform into. alpha : float Used to track the lerp relationship. Not opacity related. """ @@ -181,10 +187,22 @@ def interpolate_color(self, mobject1, mobject2, alpha): f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n" f"Mobject 2 ({mobject2}) : {mobject2.pixel_array.shape}" ) + self.fill_opacity = interpolate( + mobject1.fill_opacity, mobject2.fill_opacity, alpha + ) + self.stroke_opacity = interpolate( + mobject1.stroke_opacity, mobject2.stroke_opacity, alpha + ) self.pixel_array = interpolate( mobject1.pixel_array, mobject2.pixel_array, alpha ).astype(self.pixel_array_dtype) + def get_style(self): + return { + "fill_color": colour.rgb2hex(self.color.get_rgb()), + "fill_opacity": self.fill_opacity, + } + # TODO, add the ability to have the dimensions/orientation of this # mobject more strongly tied to the frame of the camera it contains, diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index fc7f719f5d..c70c9081fe 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -73,7 +73,7 @@ def __init__( # varying zoom levels? tolerance_for_point_equality=1e-6, n_points_per_cubic_curve=4, - **kwargs + **kwargs, ): self.fill_color = fill_color self.fill_opacity = fill_opacity @@ -485,7 +485,7 @@ def add_line_to(self, point): def add_smooth_curve_to(self, *points): """ - If two points are passed in, the first is intepretted + If two points are passed in, the first is interpreted as a handle, the second as an anchor """ if len(points) == 1: @@ -495,7 +495,7 @@ def add_smooth_curve_to(self, *points): handle2, new_anchor = points else: name = sys._getframe(0).f_code.co_name - raise ValueError("Only call {} with 1 or 2 points".format(name)) + raise ValueError(f"Only call {name} with 1 or 2 points") if self.has_new_path_started(): self.add_line_to(new_anchor) @@ -744,7 +744,7 @@ def align_points(self, vmobject): for mob in self, vmobject: # If there are no points, add one to - # whereever the "center" is + # wherever the "center" is if mob.has_no_points(): mob.start_new_path(mob.get_center()) # If there's only one point, turn it into @@ -1063,18 +1063,18 @@ def construct(self): self.wait() gr += gr2 # Add group to another self.play( - gr.shift, DOWN, + gr.animate.shift(DOWN), ) gr -= gr2 # Remove group self.play( # Animate groups separately - gr.shift, LEFT, - gr2.shift, UP, + gr.animate.shift(LEFT), + gr2.animate.shift(UP), ) self.play( #Animate groups without modification - (gr+gr2).shift, RIGHT + (gr+gr2).animate.shift(RIGHT) ) self.play( # Animate group without component - (gr-circle_red).shift, RIGHT + (gr-circle_red).animate.shift(RIGHT) ) """ if not all(isinstance(m, VMobject) for m in vmobjects): @@ -1156,14 +1156,14 @@ def construct(self): # access submobjects like a python dict my_dict["t"].set_color(PURPLE) - self.play(my_dict["t"].scale, 3) + self.play(my_dict["t"].animate.scale(3)) self.wait() # also supports python dict styled reassignment my_dict["t"] = Tex("Some other text").set_color(BLUE) self.wait() - # remove submoject by key + # remove submobject by key my_dict.remove("t") self.wait() @@ -1431,7 +1431,7 @@ def __init__( stroke_width=0, artificial_width=0.01, artificial_height=0.01, - **kwargs + **kwargs, ): self.artificial_width = artificial_width self.artificial_height = artificial_height @@ -1440,7 +1440,7 @@ def __init__( color=color, fill_opacity=fill_opacity, stroke_width=stroke_width, - **kwargs + **kwargs, ) self.set_points(np.array([location])) @@ -1458,6 +1458,22 @@ def set_location(self, new_loc): class CurvesAsSubmobjects(VGroup): + """Convert a curve's elements to submobjects. + + Examples + -------- + .. manim:: LineGradientExample + :save_last_frame: + + class LineGradientExample(Scene): + def construct(self): + curve = ParametricFunction(lambda t: [t, np.sin(t), 0], t_min = -PI, t_max=PI,stroke_width=10) + new_curve = CurvesAsSubmobjects(curve) + new_curve.set_color_by_gradient(BLUE, RED) + self.add(new_curve.shift(UP), curve) + + """ + def __init__(self, vmobject, **kwargs): VGroup.__init__(self, **kwargs) tuples = vmobject.get_cubic_bezier_tuples() @@ -1484,9 +1500,16 @@ def __init__( full_d_alpha = 1.0 / num_dashes partial_d_alpha = full_d_alpha * ps_ratio + # Shifts the alphas and removes the last dash + # to give closed shapes even spacing + if vmobject.is_closed(): + alphas += partial_d_alpha / 2 + np.delete(alphas, -1) + # Rescale so that the last point of vmobject will # be the end of the last dash - alphas /= 1 - full_d_alpha + partial_d_alpha + else: + alphas /= 1 - full_d_alpha + partial_d_alpha self.add( *[ diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 5cc2c3f790..8296bf0428 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -36,9 +36,9 @@ def construct(self): ) ) self.add(number_line, pointer,label) - self.play(pointer_value.set_value, 5) + self.play(pointer_value.animate.set_value(5)), self.wait() - self.play(pointer_value.set_value, 3) + self.play(pointer_value.animate.set_value(3)) """ diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index cb3bf18ca9..8bd16b126e 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -63,8 +63,9 @@ def __init__(self, camera_class=None, skip_animations=False, **kwargs): self.animations_hashes = [] self.num_plays = 0 self.time = 0 + self.static_image = None - def init(self, scene): + def init_scene(self, scene): self.file_writer = SceneFileWriter( self, scene.__class__.__name__, @@ -74,13 +75,13 @@ def init(self, scene): @handle_caching_play @handle_play_like_call def play(self, scene, *args, **kwargs): - scene.play_internal(*args, **kwargs) + if scene.compile_animation_data(*args, **kwargs): + scene.play_internal() def update_frame( # TODO Description in Docstring self, scene, mobjects=None, - background=None, include_submobjects=True, ignore_skipping=True, **kwargs, @@ -109,14 +110,18 @@ def update_frame( # TODO Description in Docstring scene.mobjects, scene.foreground_mobjects, ) - if background is not None: - self.camera.set_frame_to_background(background) + if self.static_image is not None: + self.camera.set_frame_to_background(self.static_image) else: self.camera.reset() kwargs["include_submobjects"] = include_submobjects self.camera.capture_mobjects(mobjects, **kwargs) + def render(self, scene, moving_mobjects): + self.update_frame(scene, moving_mobjects) + self.add_frame(self.get_frame()) + def get_frame(self): """ Gets the current frame as NumPy array. @@ -155,6 +160,11 @@ def show_frame(self): self.update_frame(ignore_skipping=True) self.camera.get_image().show() + def save_static_frame_data(self, scene, static_mobjects): + self.update_frame(scene, mobjects=static_mobjects) + self.static_image = self.get_frame() + return self.static_image + def update_skipping_status(self): """ This method is used internally to check if the current @@ -171,8 +181,7 @@ def update_skipping_status(self): self.skip_animations = True raise EndSceneEarlyException() - def finish(self, scene): - self.skip_animations = self.original_skipping_status + def scene_finished(self, scene): self.file_writer.finish() if config["save_last_frame"]: self.update_frame(scene, ignore_skipping=False) diff --git a/manim/renderer/js_renderer.py b/manim/renderer/js_renderer.py new file mode 100644 index 0000000000..21918e365a --- /dev/null +++ b/manim/renderer/js_renderer.py @@ -0,0 +1,51 @@ +import copy + + +class JsRenderer: + def __init__(self, frame_server): + self.skip_animations = True + self.frame_server = frame_server + self.camera = JsCamera() + self.num_plays = 0 + + def init_scene(self, scene): + pass + + def scene_finished(self, scene): + pass + + def play(self, scene, *args, **kwargs): + self.num_plays += 1 + s = scene.compile_animation_data(*args, skip_rendering=True, **kwargs) + scene_copy = copy.deepcopy(scene) + scene_copy.renderer = self + self.frame_server.keyframes.append(scene_copy) + if s is None: + scene_copy.is_static = True + else: + scene_copy.is_static = False + scene.play_internal(skip_rendering=True) + + def update_frame( # TODO Description in Docstring + self, + scene, + mobjects=None, + include_submobjects=True, + ignore_skipping=True, + **kwargs, + ): + pass + + def save_static_frame_data(self, scene, static_mobjects): + pass + + def add_frame(self, frame, num_frames=1): + pass + + def get_frame(self): + pass + + +class JsCamera: + def __init__(self, use_z_index=True): + self.use_z_index = use_z_index diff --git a/manim/scene/graph_scene.py b/manim/scene/graph_scene.py index afccf8795a..184614248d 100644 --- a/manim/scene/graph_scene.py +++ b/manim/scene/graph_scene.py @@ -3,10 +3,10 @@ Examples -------- -.. manim:: FunctionPlotWithLabbeledYAxis +.. manim:: FunctionPlotWithLabelledYAxis :save_last_frame: - class FunctionPlotWithLabbeledYAxis(GraphScene): + class FunctionPlotWithLabelledYAxis(GraphScene): def __init__(self, **kwargs): GraphScene.__init__( self, @@ -379,7 +379,7 @@ def parameterized_function(alpha): def input_to_graph_point(self, x, graph): """ This method returns a coordinate on the curve - given an x_value and a the graoh-curve for which + given an x_value and a the graph-curve for which the corresponding y value should be found. Parameters @@ -427,7 +427,7 @@ def angle_of_tangent(self, x, graph, dx=0.01): def slope_of_tangent(self, *args, **kwargs): """ - Returns the slople of the tangent to the plotted curve + Returns the slope of the tangent to the plotted curve at a particular x-value. Parameters @@ -656,7 +656,7 @@ def get_riemann_rectangles_list( ): """ This method returns a list of multiple VGroups of Riemann - Rectangles. The inital VGroups are relatively inaccurate, + Rectangles. The initial VGroups are relatively inaccurate, but the closer you get to the end the more accurate the Riemann rectangles become @@ -980,7 +980,7 @@ def add_T_label( side : np.array(), optional label : str, optional - The label to give the vertline and triangle + The label to give the vertical line and triangle color : str, optional The hex color of the label. diff --git a/manim/scene/moving_camera_scene.py b/manim/scene/moving_camera_scene.py index ad4cafe4e8..218a745dff 100644 --- a/manim/scene/moving_camera_scene.py +++ b/manim/scene/moving_camera_scene.py @@ -15,7 +15,7 @@ def construct(self): text = Text("Hello World").set_color(BLUE) self.add(text) self.camera_frame.save_state() - self.play(self.camera_frame.set_width, text.get_width() * 1.2) + self.play(self.camera_frame.animate.set_width(text.get_width() * 1.2)) self.wait(0.3) self.play(Restore(self.camera_frame)) @@ -28,9 +28,9 @@ def construct(self): t = Triangle(color=GREEN, fill_opacity=0.5).move_to(2 * RIGHT) self.wait(0.3) self.add(s, t) - self.play(self.camera_frame.move_to, s) + self.play(self.camera_frame.animate.move_to(s)) self.wait(0.3) - self.play(self.camera_frame.move_to, t) + self.play(self.camera_frame.animate.move_to(t)) .. manim:: MovingAndZoomingCamera @@ -40,14 +40,11 @@ def construct(self): s = Square(color=BLUE, fill_opacity=0.5).move_to(2 * LEFT) t = Triangle(color=YELLOW, fill_opacity=0.5).move_to(2 * RIGHT) self.add(s, t) - self.play(self.camera_frame.move_to, s, - self.camera_frame.set_width,s.get_width()*2) + self.play(self.camera_frame.animate.move_to(s).set_width(s.get_width()*2)) self.wait(0.3) - self.play(self.camera_frame.move_to, t, - self.camera_frame.set_width,t.get_width()*2) + self.play(self.camera_frame.animate.move_to(t).set_width(t.get_width()*2)) - self.play(self.camera_frame.move_to, ORIGIN, - self.camera_frame.set_width,14) + self.play(self.camera_frame.animate.move_to(ORIGIN).set_width(14)) .. manim:: MovingCameraOnGraph @@ -64,10 +61,10 @@ def construct(self): x_max=3 * PI ) dot_at_start_graph = Dot().move_to(graph.points[0]) - dot_at_end_grap = Dot().move_to(graph.points[-1]) - self.add(graph, dot_at_end_grap, dot_at_start_graph) - self.play(self.camera_frame.scale, 0.5, self.camera_frame.move_to, dot_at_start_graph) - self.play(self.camera_frame.move_to, dot_at_end_grap) + dot_at_end_graph = Dot().move_to(graph.points[-1]) + self.add(graph, dot_at_end_graph, dot_at_start_graph) + self.play(self.camera_frame.animate.scale(0.5).move_to(dot_at_start_graph)) + self.play(self.camera_frame.animate.move_to(dot_at_end_graph)) self.play(Restore(self.camera_frame)) self.wait() diff --git a/manim/scene/reconfigurable_scene.py b/manim/scene/reconfigurable_scene.py index 226bc30085..0a7ac386f6 100644 --- a/manim/scene/reconfigurable_scene.py +++ b/manim/scene/reconfigurable_scene.py @@ -8,7 +8,7 @@ class ReconfigurableScene(Scene): """ - Note, this seems to no longer work as intented. + Note, this seems to no longer work as intended. """ def __init__(self, allow_recursion=True, **kwargs): diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 193fb524cb..9110b54fd1 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -8,17 +8,20 @@ import random import warnings import platform +import copy +import string +import types -from tqdm import tqdm as ProgressDisplay +from tqdm import tqdm import numpy as np from .. import config, logger from ..animation.animation import Animation, Wait -from ..animation.transform import MoveToTarget +from ..animation.transform import MoveToTarget, _MethodAnimation from ..camera.camera import Camera from ..constants import * from ..container import Container -from ..mobject.mobject import Mobject +from ..mobject.mobject import Mobject, _AnimationBuilder from ..utils.iterables import list_update, list_difference_update from ..utils.family import extract_mobject_family_members from ..renderer.cairo_renderer import CairoRenderer @@ -30,7 +33,7 @@ class Scene(Container): The primary role of :class:`Scene` is to provide the user with tools to manage mobjects and animations. Generally speaking, a manim script consists of a class - that derives from :class:`Scene` whose :meth:`Scene.construct` method is overriden + that derives from :class:`Scene` whose :meth:`Scene.construct` method is overridden by the user's code. Mobjects are displayed on screen by calling :meth:`Scene.add` and removed from @@ -68,6 +71,15 @@ def __init__( self.camera_class = camera_class self.always_update_mobjects = always_update_mobjects self.random_seed = random_seed + + self.animations = None + self.stop_condition = None + self.moving_mobjects = None + self.static_mobjects = None + self.time_progression = None + self.duration = None + self.last_t = None + if renderer is None: self.renderer = CairoRenderer( camera_class=self.camera_class, @@ -75,7 +87,7 @@ def __init__( ) else: self.renderer = renderer - self.renderer.init(self) + self.renderer.init_scene(self) self.mobjects = [] # TODO, remove need for foreground mobjects @@ -90,6 +102,60 @@ def __init__( def camera(self): return self.renderer.camera + def __deepcopy__(self, clone_from_id): + cls = self.__class__ + result = cls.__new__(cls) + clone_from_id[id(self)] = result + for k, v in self.__dict__.items(): + if k in ["renderer", "time_progression"]: + continue + if k == "camera_class": + setattr(result, k, v) + setattr(result, k, copy.deepcopy(v, clone_from_id)) + + # Update updaters + for mobject in self.mobjects: + cloned_updaters = [] + for updater in mobject.updaters: + # Make the cloned updater use the cloned Mobjects as free variables + # rather than the original ones. Analyzing function bytecode with the + # dis module will help in understanding this. + # https://docs.python.org/3/library/dis.html + # TODO: Do the same for function calls recursively. + free_variable_map = inspect.getclosurevars(updater).nonlocals + cloned_co_freevars = [] + cloned_closure = [] + for i, free_variable_name in enumerate(updater.__code__.co_freevars): + free_variable_value = free_variable_map[free_variable_name] + + # If the referenced variable has not been cloned, raise. + if id(free_variable_value) not in clone_from_id: + raise Exception( + f"{free_variable_name} is referenced from an updater " + "but is not an attribute of the Scene, which isn't " + "allowed." + ) + + # Add the cloned object's name to the free variable list. + cloned_co_freevars.append(free_variable_name) + + # Add a cell containing the cloned object's reference to the + # closure list. + cloned_closure.append( + types.CellType(clone_from_id[id(free_variable_value)]) + ) + + cloned_updater = types.FunctionType( + updater.__code__.replace(co_freevars=tuple(cloned_co_freevars)), + updater.__globals__, + updater.__name__, + updater.__defaults__, + tuple(cloned_closure), + ) + cloned_updaters.append(cloned_updater) + clone_from_id[id(mobject)].updaters = cloned_updaters + return result + def render(self): """ Render this Scene. @@ -100,7 +166,8 @@ def render(self): except EndSceneEarlyException: pass self.tear_down() - self.renderer.finish(self) + # We have to reset these settings in case of multiple renders. + self.renderer.scene_finished(self) logger.info( f"Rendered {str(self)}\nPlayed {self.renderer.num_plays} animations" ) @@ -108,7 +175,7 @@ def render(self): def setup(self): """ This is meant to be implemented by any scenes which - are comonly subclassed, and have some common setup + are commonly subclassed, and have some common setup involved before the construct method is called. """ pass @@ -116,7 +183,7 @@ def setup(self): def tear_down(self): """ This is meant to be implemented by any scenes which - are comonly subclassed, and have some common method + are commonly subclassed, and have some common method to be invoked before the scene ends. """ pass @@ -137,7 +204,7 @@ def construct(self): Examples -------- A typical manim script includes a class derived from :class:`Scene` with an - overriden :meth:`Scene.contruct` method: + overridden :meth:`Scene.contruct` method: .. code-block:: python @@ -252,6 +319,11 @@ def add(self, *mobjects): mobjects = [*mobjects, *self.foreground_mobjects] self.restructure_mobjects(to_remove=mobjects) self.mobjects += mobjects + if self.moving_mobjects: + self.restructure_mobjects( + to_remove=mobjects, mobject_list_name="moving_mobjects" + ) + self.moving_mobjects += mobjects return self def add_mobjects_from_animations(self, animations): @@ -512,8 +584,7 @@ def get_moving_mobjects(self, *animations): return mobjects[i:] return [] - def get_moving_and_stationary_mobjects(self, animations): - moving_mobjects = self.get_moving_mobjects(*animations) + def get_moving_and_static_mobjects(self, animations): all_mobjects = list_update(self.mobjects, self.foreground_mobjects) all_mobject_families = extract_mobject_family_members( all_mobjects, @@ -525,92 +596,97 @@ def get_moving_and_stationary_mobjects(self, animations): moving_mobjects, use_z_index=self.renderer.camera.use_z_index, ) - stationary_mobjects = list_difference_update( + static_mobjects = list_difference_update( all_mobject_families, all_moving_mobject_families ) - return all_moving_mobject_families, stationary_mobjects + return all_moving_mobject_families, static_mobjects - def compile_play_args_to_animation_list(self, *args, **kwargs): + def compile_animations(self, *args, **kwargs): """ - Each arg can either be an animation, or a mobject method - followed by that methods arguments (and potentially follow - by a dict of kwargs for that method). - This animation list is built by going through the args list, - and each animation is simply added, but when a mobject method - is hit, a MoveToTarget animation is built using the args that - follow up until either another animation is hit, another method - is hit, or the args list runs out. - + Creates _MethodAnimations from any _AnimationBuilders and updates animation + kwargs with kwargs passed to play(). Parameters ---------- - *args : Animation or method of a mobject, which is followed by that method's arguments - - **kwargs : any named arguments like run_time or lag_ratio. - + *animations : Tuple[:class:`Animation`] + Animations to be played. + **play_kwargs + Configuration for the call to play(). Returns ------- - list : list of animations with the parameters applied to them. + Tuple[:class:`Animation`] + Animations to be played. """ animations = [] - state = { - "curr_method": None, - "last_method": None, - "method_args": [], - } - - def compile_method(state): - if state["curr_method"] is None: - return - mobject = state["curr_method"].__self__ - if state["last_method"] and state["last_method"].__self__ is mobject: - animations.pop() - # method should already have target then. - else: - mobject.generate_target() - # - if len(state["method_args"]) > 0 and isinstance( - state["method_args"][-1], dict - ): - method_kwargs = state["method_args"].pop() - else: - method_kwargs = {} - state["curr_method"].__func__( - mobject.target, *state["method_args"], **method_kwargs - ) - animations.append(MoveToTarget(mobject)) - state["last_method"] = state["curr_method"] - state["curr_method"] = None - state["method_args"] = [] - for arg in args: - if isinstance(arg, Animation): - compile_method(state) + if isinstance(arg, _AnimationBuilder): + animations.append(arg.build()) + elif isinstance(arg, Animation): animations.append(arg) elif inspect.ismethod(arg): - compile_method(state) - state["curr_method"] = arg - elif state["curr_method"] is not None: - state["method_args"].append(arg) - elif isinstance(arg, Mobject): - raise ValueError( - """ - I think you may have invoked a method - you meant to pass in as a Scene.play argument - """ + raise TypeError( + "Passing Mobject methods to Scene.play is no longer supported. Use " + "Mobject.animate instead." ) else: - raise ValueError("Invalid play arguments") - compile_method(state) + raise TypeError(f"Unexpected argument {arg} passed to Scene.play().") for animation in animations: - # This is where kwargs to play like run_time and rate_func - # get applied to all animations - animation.update_config(**kwargs) + for k, v in kwargs.items(): + setattr(animation, k, v) return animations + def _get_animation_time_progression(self, animations, duration): + """ + You will hardly use this when making your own animations. + This method is for Manim's internal use. + + Uses :func:`~.get_time_progression` to obtain a + CommandLine ProgressBar whose ``fill_time`` is + dependent on the qualities of the passed Animation, + + Parameters + ---------- + animations : List[:class:`~.Animation`, ...] + The list of animations to get + the time progression for. + + duration : int or float + duration of wait time + + Returns + ------- + time_progression + The CommandLine Progress Bar. + """ + if len(animations) == 1 and isinstance(animations[0], Wait): + stop_condition = animations[0].stop_condition + if stop_condition is not None: + time_progression = self.get_time_progression( + duration, + f"Waiting for {stop_condition.__name__}", + n_iterations=-1, # So it doesn't show % progress + override_skip_animations=True, + ) + else: + time_progression = self.get_time_progression( + duration, f"Waiting {self.renderer.num_plays}" + ) + else: + time_progression = self.get_time_progression( + duration, + "".join( + [ + f"Animation {self.renderer.num_plays}: ", + str(animations[0]), + (", etc." if len(animations) > 1 else ""), + ] + ), + ) + return time_progression + def get_time_progression( - self, run_time, n_iterations=None, override_skip_animations=False + self, run_time, description, n_iterations=None, override_skip_animations=False ): """ You will hardly use this when making your own animations. @@ -635,7 +711,7 @@ def get_time_progression( Returns ------- - ProgressDisplay + time_progression The CommandLine Progress Bar. """ if self.renderer.skip_animations and not override_skip_animations: @@ -643,7 +719,7 @@ def get_time_progression( else: step = 1 / self.renderer.camera.frame_rate times = np.arange(0, run_time, step) - time_progression = ProgressDisplay( + time_progression = tqdm( times, total=n_iterations, leave=config["leave_progress_bars"], @@ -652,74 +728,6 @@ def get_time_progression( ) return time_progression - def _get_animation_time_progression(self, animations): - """ - You will hardly use this when making your own animations. - This method is for Manim's internal use. - - Uses :func:`~.get_time_progression` to obtain a - CommandLine ProgressBar whose ``fill_time`` is - dependent on the qualities of the passed Animation, - - Parameters - ---------- - animations : List[:class:`~.Animation`, ...] - The list of animations to get - the time progression for. - - Returns - ------- - ProgressDisplay - The CommandLine Progress Bar. - """ - run_time = self.get_run_time(animations) - time_progression = self.get_time_progression(run_time) - time_progression.set_description( - "".join( - [ - "Animation {}: ".format(self.renderer.num_plays), - str(animations[0]), - (", etc." if len(animations) > 1 else ""), - ] - ) - ) - return time_progression - - def _get_wait_time_progression(self, duration, stop_condition): - """ - This method is used internally to obtain the CommandLine - Progressbar for when self.wait() is called in a scene. - - Parameters - ---------- - duration : int or float - duration of wait time - - stop_condition : function - The function which determines whether to continue waiting. - - Returns - ------- - ProgressBar - The CommandLine ProgressBar of the wait time - - """ - if stop_condition is not None: - time_progression = self.get_time_progression( - duration, - n_iterations=-1, # So it doesn't show % progress - override_skip_animations=True, - ) - time_progression.set_description( - "Waiting for {}".format(stop_condition.__name__) - ) - else: - time_progression = self.get_time_progression(duration) - time_progression.set_description( - "Waiting {}".format(self.renderer.num_plays) - ) - return time_progression - def get_run_time(self, animations): """ Gets the total run time for a list of animations. @@ -736,7 +744,14 @@ def get_run_time(self, animations): The total ``run_time`` of all of the animations in the list. """ - return np.max([animation.run_time for animation in animations]) + if len(animations) == 1 and isinstance(animations[0], Wait): + if animations[0].stop_condition is not None: + return 0 + else: + return animations[0].duration + + else: + return np.max([animation.run_time for animation in animations]) def play(self, *args, **kwargs): self.renderer.play(self, *args, **kwargs) @@ -760,7 +775,50 @@ def wait_until(self, stop_condition, max_time=60): """ self.wait(max_time, stop_condition=stop_condition) - def play_internal(self, *args, **kwargs): + def compile_animation_data(self, *animations, skip_rendering=False, **play_kwargs): + if len(animations) == 0: + warnings.warn("Called Scene.play with no animations") + return None + + self.animations = self.compile_animations(*animations, **play_kwargs) + self.add_mobjects_from_animations(self.animations) + + self.last_t = 0 + self.stop_condition = None + self.moving_mobjects = None + self.static_mobjects = None + if len(self.animations) == 1 and isinstance(self.animations[0], Wait): + self.update_mobjects(dt=0) # Any problems with this? + if self.should_update_mobjects(): + # TODO, be smart about setting a static image + # the same way Scene.play does + self.renderer.static_image = None + self.stop_condition = self.animations[0].stop_condition + else: + self.duration = self.animations[0].duration + if not skip_rendering: + self.add_static_frames(self.animations[0].duration) + return None + else: + # Paint all non-moving objects onto the screen, so they don't + # have to be rendered every frame + ( + self.moving_mobjects, + self.static_mobjects, + ) = self.get_moving_and_static_mobjects(self.animations) + self.renderer.save_static_frame_data(self, self.static_mobjects) + + self.duration = self.get_run_time(self.animations) + self.time_progression = self._get_animation_time_progression( + self.animations, self.duration + ) + + for animation in self.animations: + animation.begin() + + return self + + def play_internal(self, skip_rendering=False): """ This method is used to prep the animations for rendering, apply the arguments and parameters required to them, @@ -774,63 +832,27 @@ def play_internal(self, *args, **kwargs): named parameters affecting what was passed in ``args``, e.g. ``run_time``, ``lag_ratio`` and so on. """ - if len(args) == 0: - warnings.warn("Called Scene.play with no animations") - return - - animations = self.compile_play_args_to_animation_list(*args, **kwargs) - if ( - len(animations) == 1 - and isinstance(animations[0], Wait) - and not self.should_update_mobjects() - ): - self.add_static_frames(animations[0].duration) - return - - for animation in animations: - animation.begin() - - moving_mobjects = None - static_mobjects = None - duration = None - stop_condition = None - time_progression = None - if len(animations) == 1 and isinstance(animations[0], Wait): - # TODO, be smart about setting a static image - # the same way Scene.play does - duration = animations[0].duration - stop_condition = animations[0].stop_condition - self.static_image = None - time_progression = self._get_wait_time_progression(duration, stop_condition) - else: - # Paint all non-moving objects onto the screen, so they don't - # have to be rendered every frame - ( - moving_mobjects, - stationary_mobjects, - ) = self.get_moving_and_stationary_mobjects(animations) - self.renderer.update_frame(self, mobjects=stationary_mobjects) - self.static_image = self.renderer.get_frame() - time_progression = self._get_animation_time_progression(animations) - - last_t = 0 - for t in time_progression: - dt = t - last_t - last_t = t - for animation in animations: - animation.update_mobjects(dt) - alpha = t / animation.run_time - animation.interpolate(alpha) - self.update_mobjects(dt) - self.renderer.update_frame(self, moving_mobjects, self.static_image) - self.renderer.add_frame(self.renderer.get_frame()) - if stop_condition is not None and stop_condition(): - time_progression.close() + for t in self.time_progression: + self.update_to_time(t) + if not skip_rendering: + self.renderer.render(self, self.moving_mobjects) + if self.stop_condition is not None and self.stop_condition(): + self.time_progression.close() break - for animation in animations: + for animation in self.animations: animation.finish() animation.clean_up_from_scene(self) + self.renderer.static_image = None + + def update_to_time(self, t): + dt = t - self.last_t + self.last_t = t + for animation in self.animations: + animation.update_mobjects(dt) + alpha = t / animation.run_time + animation.interpolate(alpha) + self.update_mobjects(dt) def add_static_frames(self, duration): self.renderer.update_frame(self) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index a8bfe4019f..a99ac38a50 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -124,10 +124,7 @@ def add_partial_movie_file(self, hash_animation): return new_partial_movie_file = os.path.join( self.partial_movie_directory, - "{}{}".format( - hash_animation, - config["movie_file_extension"], - ), + f"{hash_animation}{config['movie_file_extension']}", ) self.partial_movie_files.append(new_partial_movie_file) @@ -158,7 +155,7 @@ def get_resolution_directory(self): """ pixel_height = config["pixel_height"] frame_rate = config["frame_rate"] - return "{}p{}".format(pixel_height, frame_rate) + return f"{pixel_height}p{frame_rate}" # Sound def init_audio(self): @@ -335,7 +332,7 @@ def finish(self): def open_movie_pipe(self): """ - Used internally by Manim to initalise + Used internally by Manim to initialise FFMPEG and begin writing to FFMPEG's input buffer. """ @@ -358,7 +355,7 @@ def open_movie_pipe(self): "-r", str(fps), # frames per second "-i", - "-", # The imput comes from a pipe + "-", # The input comes from a pipe "-an", # Tells FFMPEG not to expect any audio "-loglevel", config["ffmpeg_loglevel"].lower(), @@ -399,7 +396,7 @@ def is_already_cached(self, hash_invocation): return False path = os.path.join( self.partial_movie_directory, - "{}{}".format(hash_invocation, config["movie_file_extension"]), + f"{hash_invocation}{config['movie_file_extension']}", ) return os.path.exists(path) @@ -416,7 +413,7 @@ def combine_movie_files(self): # viewing the scene as a whole, one of course wants to see it as a # single piece. partial_movie_files = [el for el in self.partial_movie_files if el is not None] - # NOTE : Here we should do a check and raise an exeption if partial + # NOTE : Here we should do a check and raise an exception if partial # movie file is empty. We can't, as a lot of stuff (in particular, in # tests) use scene initialization, and this error would be raised as # it's just an empty scene initialized. @@ -435,7 +432,7 @@ def combine_movie_files(self): for pf_path in partial_movie_files: if os.name == "nt": pf_path = pf_path.replace("\\", "/") - fp.write("file 'file:{}'\n".format(pf_path)) + fp.write(f"file 'file:{pf_path}'\n") movie_file_path = self.movie_file_path commands = [ FFMPEG_BIN, diff --git a/manim/scene/three_d_scene.py b/manim/scene/three_d_scene.py index 613d5b3357..2ecd582042 100644 --- a/manim/scene/three_d_scene.py +++ b/manim/scene/three_d_scene.py @@ -69,7 +69,7 @@ def set_camera_orientation(self, phi=None, theta=None, distance=None, gamma=None if gamma is not None: self.renderer.camera.set_gamma(gamma) - def begin_ambient_camera_rotation(self, rate=0.02): + def begin_ambient_camera_rotation(self, rate=0.02, about="theta"): """ This method begins an ambient rotation of the camera about the Z_AXIS, in the anticlockwise direction @@ -79,32 +79,50 @@ def begin_ambient_camera_rotation(self, rate=0.02): rate : int or float, optional The rate at which the camera should rotate about the Z_AXIS. Negative rate means clockwise rotation. + about: (str) + one of 3 options: ["theta", "phi", "gamma"]. defaults to theta. """ # TODO, use a ValueTracker for rate, so that it # can begin and end smoothly - self.renderer.camera.theta_tracker.add_updater( - lambda m, dt: m.increment_value(rate * dt) - ) - self.add(self.renderer.camera.theta_tracker) + if about.lower() == "phi": + x = self.renderer.camera.phi_tracker + elif about.lower() == "gamma": + x = self.renderer.camera.gamma_tracker + elif about.lower() == "theta": + x = self.renderer.camera.theta_tracker + else: + raise ValueError("Invalid ambient rotation angle.") + + x.add_updater(lambda m, dt: m.increment_value(rate * dt)) + self.add(x) - def stop_ambient_camera_rotation(self): + def stop_ambient_camera_rotation(self, about="theta"): """ This method stops all ambient camera rotation. """ - self.renderer.camera.theta_tracker.clear_updaters() - self.remove(self.renderer.camera.theta_tracker) + if about.lower() == "phi": + x = self.renderer.camera.phi_tracker + elif about.lower() == "gamma": + x = self.renderer.camera.gamma_tracker + elif about.lower() == "theta": + x = self.renderer.camera.theta_tracker + else: + raise ValueError("Invalid ambient rotation angle.") + + x.clear_updaters() + self.remove(x) def begin_3dillusion_camera_rotation( self, rate=1, origin_theta=-60 * DEGREES, origin_phi=75 * DEGREES ): val_tracker_theta = ValueTracker(0) - def uptate_theta(m, dt): + def update_theta(m, dt): val_tracker_theta.increment_value(dt * rate) val_for_left_right = 0.2 * np.sin(val_tracker_theta.get_value()) return m.set_value(origin_theta + val_for_left_right) - self.renderer.camera.theta_tracker.add_updater(uptate_theta) + self.renderer.camera.theta_tracker.add_updater(update_theta) self.add(self.renderer.camera.theta_tracker) val_tracker_phi = ValueTracker(0) @@ -219,7 +237,7 @@ def add_fixed_in_frame_mobjects(self, *mobjects): """ This method is used to prevent the rotation and movement of mobjects as the camera moves around. The mobject is - essentially overlayed, and is not impacted by the camera's + essentially overlaid, and is not impacted by the camera's movement in any way. Parameters diff --git a/manim/scene/vector_space_scene.py b/manim/scene/vector_space_scene.py index 4d15d14665..f0c7dbbe5c 100644 --- a/manim/scene/vector_space_scene.py +++ b/manim/scene/vector_space_scene.py @@ -1073,7 +1073,7 @@ def apply_function(self, function, added_anims=[], **kwargs): added_anims : list, optional Any other animations that need to be played - simulataneously with this. + simultaneously with this. **kwargs Any valid keyword argument of a self.play() call. diff --git a/manim/scene/zoomed_scene.py b/manim/scene/zoomed_scene.py index fbe73cf20b..df8576629c 100644 --- a/manim/scene/zoomed_scene.py +++ b/manim/scene/zoomed_scene.py @@ -13,7 +13,7 @@ def construct(self): self.wait(1) self.activate_zooming(animate=False) self.wait(1) - self.play(dot.shift, LEFT) + self.play(dot.animate.shift(LEFT)) .. manim:: ChangingZoomScale @@ -38,10 +38,10 @@ def construct(self): self.wait(1) self.activate_zooming(animate=False) self.wait(1) - self.play(dot.shift, LEFT * 0.3) + self.play(dot.animate.shift(LEFT * 0.3)) - self.play(self.zoomed_camera.frame.scale, 4) - self.play(self.zoomed_camera.frame.shift, 0.5 * DOWN) + self.play(self.zoomed_camera.frame.animate.scale(4)) + self.play(self.zoomed_camera.frame.animate.shift(0.5 * DOWN)) """ diff --git a/manim/utils/bezier.py b/manim/utils/bezier.py index 4a2684d523..ad10f149dd 100644 --- a/manim/utils/bezier.py +++ b/manim/utils/bezier.py @@ -115,7 +115,7 @@ def get_smooth_handle_points( l, u = 2, 1 # diag is a representation of the matrix in diagonal form # See https://www.particleincell.com/2012/bezier-splines/ - # for how to arive at these equations + # for how to arrive at these equations diag = np.zeros((l + u + 1, 2 * num_handles)) diag[0, 1::2] = -1 diag[0, 2::2] = 1 diff --git a/manim/utils/caching.py b/manim/utils/caching.py index 72a3b7ac51..459406e83b 100644 --- a/manim/utils/caching.py +++ b/manim/utils/caching.py @@ -20,7 +20,7 @@ def handle_caching_play(func): def wrapper(self, scene, *args, **kwargs): self.skip_animations = self.original_skipping_status self.update_skipping_status() - animations = scene.compile_play_args_to_animation_list(*args, **kwargs) + animations = scene.compile_animations(*args, **kwargs) scene.add_mobjects_from_animations(animations) if self.skip_animations: logger.debug(f"Skipping animation {self.num_plays}") @@ -42,7 +42,7 @@ def wrapper(self, scene, *args, **kwargs): ) self.skip_animations = True else: - hash_play = "uncached_{:05}".format(self.num_plays) + hash_play = f"uncached_{self.num_plays:05}" self.animations_hashes.append(hash_play) self.file_writer.add_partial_movie_file(hash_play) logger.debug( diff --git a/manim/utils/config_ops.py b/manim/utils/config_ops.py index c3e6ac0c8a..b6d40afcfc 100644 --- a/manim/utils/config_ops.py +++ b/manim/utils/config_ops.py @@ -1,4 +1,4 @@ -"""Utilities that mught be useful for configuration dictionaries.""" +"""Utilities that might be useful for configuration dictionaries.""" __all__ = [ "merge_dicts_recursively", diff --git a/manim/utils/family.py b/manim/utils/family.py index 428f2c816d..3c01fb52f8 100644 --- a/manim/utils/family.py +++ b/manim/utils/family.py @@ -28,6 +28,9 @@ def extract_mobject_family_members( method = Mobject.family_members_with_points else: method = Mobject.get_family + extracted_mobjects = remove_list_redundancies( + list(it.chain(*[method(m) for m in mobjects])) + ) if use_z_index: - mobjects = sorted(mobjects, key=lambda m: m.z_index) - return remove_list_redundancies(list(it.chain(*[method(m) for m in mobjects]))) + return sorted(extracted_mobjects, key=lambda m: m.z_index) + return extracted_mobjects diff --git a/manim/utils/file_ops.py b/manim/utils/file_ops.py index 2296a6bf0f..6b450cb424 100644 --- a/manim/utils/file_ops.py +++ b/manim/utils/file_ops.py @@ -37,9 +37,7 @@ def seek_full_path_from_defaults(file_name, default_dir, extensions): for path in possible_paths: if os.path.exists(path): return path - error = "From: {}, could not find {} at either of these locations: {}".format( - os.getcwd(), file_name, possible_paths - ) + error = f"From: {os.getcwd()}, could not find {file_name} at either of these locations: {possible_paths}" raise IOError(error) diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index f7749fdaed..e0571c7797 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -60,8 +60,8 @@ def default(self, obj): return repr(obj) elif hasattr(obj, "__dict__"): temp = getattr(obj, "__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 instancied - # Indeed, there is certainly no case where scene-caching will recieve only a non instancied object, as this is never used in the library or encouraged to be used user-side. + # 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): return "MappingProxy" return self._check_iterable(temp) @@ -204,9 +204,9 @@ def get_camera_dict_for_hashing(camera_object): `Camera.__dict__` but cleaned. """ camera_object_dict = copy.copy(camera_object.__dict__) - # We have to clean a little bit of camera_dict, as pixel_array and background are two very big numpy arrays. - # They are not essential to caching process. - # We also have to remove pixel_array_to_cairo_context as it contains used memory adress (set randomly). See l.516 get_cached_cairo_context in camera.py + # We have to clean a little bit of camera_dict, as pixel_array and background are two very big numpy arrays. They + # are not essential to caching process. We also have to remove pixel_array_to_cairo_context as it contains used + # memory address (set randomly). See l.516 get_cached_cairo_context in camera.py for to_clean in ["background", "pixel_array", "pixel_array_to_cairo_context"]: camera_object_dict.pop(to_clean, None) return camera_object_dict @@ -251,7 +251,7 @@ def get_hash_from_play_call( t_end = perf_counter() logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]}) hash_complete = f"{hash_camera}_{hash_animations}_{hash_current_mobjects}" - # This will reset ALREADY_PROCESSED_ID as all the hashing processus is finished. + # This will reset ALREADY_PROCESSED_ID as all the hashing process is finished. ALREADY_PROCESSED_ID = {} logger.debug("Hash generated : %(h)s", {"h": hash_complete}) return hash_complete @@ -293,7 +293,7 @@ def get_hash_from_wait_call( hash_camera = zlib.crc32(repr(camera_json).encode()) if stop_condition_function is not None: hash_function = zlib.crc32(get_json(stop_condition_function).encode()) - # This will reset ALREADY_PROCESSED_ID as all the hashing processus is finished. + # This will reset ALREADY_PROCESSED_ID as all the hashing process is finished. ALREADY_PROCESSED_ID = {} t_end = perf_counter() logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]}) diff --git a/manim/utils/iterables.py b/manim/utils/iterables.py index 5919479ffc..a82ba60e8c 100644 --- a/manim/utils/iterables.py +++ b/manim/utils/iterables.py @@ -23,7 +23,7 @@ def remove_list_redundancies(l): """ Used instead of list(set(l)) to maintain order - Keeps the last occurance of each element + Keeps the last occurrence of each element """ reversed_result = [] used = set() diff --git a/manim/utils/module_ops.py b/manim/utils/module_ops.py index 99faea529c..eafc30fbe2 100644 --- a/manim/utils/module_ops.py +++ b/manim/utils/module_ops.py @@ -105,3 +105,13 @@ def prompt_user_for_choice(scene_classes): sys.exit(2) except EOFError: sys.exit(1) + + +def scene_classes_from_file(file_path, require_single_scene=False): + module = get_module(file_path) + all_scene_classes = get_scene_classes_from_module(module) + scene_classes_to_render = get_scenes_to_render(all_scene_classes) + if require_single_scene: + assert len(scene_classes_to_render) == 1 + return scene_classes_to_render[0] + return scene_classes_to_render diff --git a/manim/utils/tex.py b/manim/utils/tex.py index 8301a6c167..6804e10400 100644 --- a/manim/utils/tex.py +++ b/manim/utils/tex.py @@ -21,7 +21,7 @@ class TexTemplate: documentclass : Optional[:class:`str`], optional The command defining the documentclass, e.g. ``\\documentclass[preview]{standalone}`` preamble : Optional[:class:`str`], optional - The document's preample, i.e. the part between ``\\documentclass`` and ``\\begin{document}`` + The document's preamble, i.e. the part between ``\\documentclass`` and ``\\begin{document}`` placeholder_text : Optional[:class:`str`], optional Text in the document that will be replaced by the expression to be rendered post_doc_commands : Optional[:class:`str`], optional @@ -78,7 +78,7 @@ def __init__( preamble=None, placeholder_text=None, post_doc_commands=None, - **kwargs + **kwargs, ): self.tex_compiler = ( tex_compiler @@ -224,9 +224,7 @@ def get_texcode_for_expression_in_env(self, expression, environment): LaTeX code based on template, containing the given expression inside its environment, ready for typesetting """ begin, end = self._texcode_for_environment(environment) - return self.body.replace( - self.placeholder_text, "{0}\n{1}\n{2}".format(begin, expression, end) - ) + return self.body.replace(self.placeholder_text, f"{begin}\n{expression}\n{end}") class TexTemplateFromFile(TexTemplate): @@ -241,7 +239,7 @@ class TexTemplateFromFile(TexTemplate): documentclass : Optional[:class:`str`], optional The command defining the documentclass, e.g. ``\\documentclass[preview]{standalone}`` preamble : Optional[:class:`str`], optional - The document's preample, i.e. the part between ``\\documentclass`` and ``\\begin{document}`` + The document's preamble, i.e. the part between ``\\documentclass`` and ``\\begin{document}`` placeholder_text : Optional[:class:`str`], optional Text in the document that will be replaced by the expression to be rendered post_doc_commands : Optional[:class:`str`], optional diff --git a/manim/utils/tex_file_writing.py b/manim/utils/tex_file_writing.py index 80b311be29..3ca111248f 100644 --- a/manim/utils/tex_file_writing.py +++ b/manim/utils/tex_file_writing.py @@ -126,8 +126,8 @@ def tex_compilation_command(tex_compiler, output_format, tex_file, tex_dir): outflag, "-interaction=batchmode", "-halt-on-error", - '-output-directory="{}"'.format(tex_dir), - '"{}"'.format(tex_file), + f'-output-directory="{tex_dir}"', + f'"{tex_file}"', ">", os.devnull, ] @@ -197,7 +197,7 @@ def convert_to_svg(dvi_file, extension, page=1): "dvisvgm", "--pdf" if extension == ".pdf" else "", "-p " + str(page), - '"{}"'.format(dvi_file), + f'"{dvi_file}"', "-n", "-v 0", "-o " + f'"{result}"', diff --git a/poetry.lock b/poetry.lock index fe77424bb4..b845c9ac79 100644 --- a/poetry.lock +++ b/poetry.lock @@ -84,48 +84,21 @@ typing-extensions = ">=3.7.4" colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] -[[package]] -name = "cairocffi" -version = "1.2.0" -description = "cffi-based cairo bindings for Python" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.1.0" - -[package.extras] -doc = ["sphinx", "sphinx-rtd-theme"] -test = ["pytest-runner", "pytest-cov", "pytest-flake8", "pytest-isort"] -xcb = ["xcffib (>=0.3.2)"] - [[package]] name = "certifi" -version = "2020.11.8" +version = "2020.12.5" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = "*" -[[package]] -name = "cffi" -version = "1.14.4" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - [[package]] name = "chardet" -version = "3.0.4" +version = "4.0.0" description = "Universal encoding detector for Python 2 and 3" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "click" @@ -184,6 +157,14 @@ category = "main" optional = false python-versions = ">=3.6, <3.7" +[[package]] +name = "decorator" +version = "4.4.2" +description = "Decorators for Humans" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" + [[package]] name = "docutils" version = "0.16" @@ -247,18 +228,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "3.1.0" +version = "3.3.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -270,7 +252,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.6.4" +version = "5.7.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -311,6 +293,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "manimpango" +version = "0.1.4" +description = "Bindings for Pango for using with Manim." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "markupsafe" version = "1.1.1" @@ -351,6 +341,30 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "networkx" +version = "2.5" +description = "Python package for creating and manipulating graphs and networks" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +decorator = ">=4.3.0" + +[package.extras] +all = ["numpy", "scipy", "pandas", "matplotlib", "pygraphviz", "pydot", "pyyaml", "lxml", "pytest"] +gdal = ["gdal"] +lxml = ["lxml"] +matplotlib = ["matplotlib"] +numpy = ["numpy"] +pandas = ["pandas"] +pydot = ["pydot"] +pygraphviz = ["pygraphviz"] +pytest = ["pytest"] +pyyaml = ["pyyaml"] +scipy = ["scipy"] + [[package]] name = "numpy" version = "1.19.4" @@ -361,7 +375,7 @@ python-versions = ">=3.6" [[package]] name = "packaging" -version = "20.7" +version = "20.8" description = "Core utilities for Python packages" category = "dev" optional = false @@ -370,30 +384,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" -[[package]] -name = "pangocairocffi" -version = "0.4.0" -description = "CFFI-based pango-cairo bindings for Python" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cairocffi = ">=1.0.2" -cffi = ">=1.1.0" -pangocffi = ">=0.8.0" - -[[package]] -name = "pangocffi" -version = "0.8.0" -description = "CFFI-based pango bindings for Python" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.1.0" - [[package]] name = "pathspec" version = "0.8.1" @@ -402,14 +392,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "pathtools" -version = "0.1.2" -description = "File system general utilities" -category = "main" -optional = true -python-versions = "*" - [[package]] name = "pillow" version = "8.0.1" @@ -453,7 +435,7 @@ six = ">=1.9" [[package]] name = "py" -version = "1.9.0" +version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false @@ -467,14 +449,6 @@ category = "main" optional = false python-versions = ">=3.6, <4" -[[package]] -name = "pycparser" -version = "2.20" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "pydub" version = "0.24.1" @@ -485,7 +459,7 @@ python-versions = "*" [[package]] name = "pygments" -version = "2.7.2" +version = "2.7.3" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -516,25 +490,24 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.1.2" +version = "6.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" +attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0" +pluggy = ">=0.12,<1.0.0a1" py = ">=1.8.2" toml = "*" [package.extras] -checkqa_mypy = ["mypy (0.780)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -550,7 +523,7 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2020.4" +version = "2020.5" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -558,7 +531,7 @@ python-versions = "*" [[package]] name = "recommonmark" -version = "0.6.0" +version = "0.7.1" description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." category = "dev" optional = false @@ -579,7 +552,7 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.0" +version = "2.25.1" description = "Python HTTP for Humans." category = "dev" optional = false @@ -587,7 +560,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" +chardet = ">=3.0.2,<5" idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.27" @@ -642,7 +615,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "3.3.1" +version = "3.4.1" description = "Python documentation generator" category = "dev" optional = false @@ -668,8 +641,8 @@ sphinxcontrib-serializinghtml = "*" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.790)", "docutils-stubs"] -test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.790)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinxcontrib-applehelp" @@ -752,18 +725,19 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tqdm" -version = "4.54.0" +version = "4.55.0" description = "Fast, Extensible Progress Meter" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown", "wheel"] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +telegram = ["requests"] [[package]] name = "typed-ast" -version = "1.4.1" +version = "1.4.2" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -792,14 +766,11 @@ socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] name = "watchdog" -version = "0.10.4" +version = "1.0.2" description = "Filesystem events monitoring" category = "main" optional = true -python-versions = "*" - -[package.dependencies] -pathtools = ">=0.1.1" +python-versions = ">=3.6" [package.extras] watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] @@ -830,7 +801,7 @@ js_renderer = ["grpcio", "grpcio-tools", "watchdog"] [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "f66610f54761f5f832bba54c4b8fb33ca584624493965927935763b24df6e08f" +content-hash = "a93e116c1f18b3051247c3f05c79d966b2b846e018d23284e4198422a33419d6" [metadata.files] alabaster = [ @@ -860,52 +831,13 @@ babel = [ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] -cairocffi = [ - {file = "cairocffi-1.2.0.tar.gz", hash = "sha256:9a979b500c64c8179fec286f337e8fe644eca2f2cd05860ce0b62d25f22ea140"}, -] certifi = [ - {file = "certifi-2020.11.8-py2.py3-none-any.whl", hash = "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd"}, - {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"}, -] -cffi = [ - {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, - {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, - {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, - {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, - {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, - {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, - {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, - {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, - {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, - {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, - {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, - {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, - {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, - {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, - {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, - {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, - {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, - {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, - {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, - {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, - {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, - {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, - {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, - {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, - {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, - {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, - {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, - {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, - {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, - {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, - {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -931,6 +863,10 @@ dataclasses = [ {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, ] +decorator = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, @@ -1043,16 +979,16 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"}, - {file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"}, + {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, + {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, - {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, + {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"}, + {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"}, ] jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, @@ -1115,6 +1051,30 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, ] +manimpango = [ + {file = "manimpango-0.1.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4d61797b36b9938c226bd38a32fd15eea580bf052044143749326921bf0a6cd"}, + {file = "manimpango-0.1.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:fcbe104da7b63d8781d1e9952cba6bf4358a9c341c4e98f158bc91b913384f91"}, + {file = "manimpango-0.1.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:6e02472fbb05176ba5d0b70866221152507fe6daa4bdf1eb1cf30dffb6f606bd"}, + {file = "manimpango-0.1.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:05a055c41db3b731cbd3fcb3cfe7b1765d4bb1c3b6201db392a3ee9e57772976"}, + {file = "manimpango-0.1.4-cp36-cp36m-win32.whl", hash = "sha256:3cb3622f4defdcbb0c90bbd86aa20ec2ae9b548a967d31a49f5a06a848b5b923"}, + {file = "manimpango-0.1.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4059eac98f93296da9431a2dd3c2e863819a311cc945d3c8e6112a48e4657005"}, + {file = "manimpango-0.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f489bc3f5fe0c908894a480ed14e5ac0cc87a959aeb073cf5a82a48eecd07bcb"}, + {file = "manimpango-0.1.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:c3b493dd5cf53f0243c3d7d861415869bac7e04ec86a816536d23aab2aa88230"}, + {file = "manimpango-0.1.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:205e7ded2e74f174be4d1e96b27a2fc3bf9b0bdfedccc095b79d627d659d4238"}, + {file = "manimpango-0.1.4-cp37-cp37m-win32.whl", hash = "sha256:4d5eb4a98684dfd91809c0c910637a952c14c35e0029776e4f907aaf61c206fa"}, + {file = "manimpango-0.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:306a8e67782178f968e5f1414f4b86711c487ea74f886280aae93225afa103b9"}, + {file = "manimpango-0.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:75044532210a11c00cfd5a6adc5381fd9b848b4a4f444b5d872488d571c6ca2a"}, + {file = "manimpango-0.1.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:34867ab61eb691dae7291596eb09c12616ee054f9812e884a0391c83cf6b5f92"}, + {file = "manimpango-0.1.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:7ca4adcebe45b37e52edb7d5bdc50e0b53fc73eada17e6dfc83e237386df4e36"}, + {file = "manimpango-0.1.4-cp38-cp38-win32.whl", hash = "sha256:8543495da7b7fb5c35a33af710130b2c6950a1b931b5baf03c058573657b1197"}, + {file = "manimpango-0.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:a840b41243d30d8d19a4847c2213aa05158d3009dbc4289b914eac92719b4f24"}, + {file = "manimpango-0.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c17cb10e74f191df7a7848423c298e5a4004a13641088a6a31b4ae5296f00d82"}, + {file = "manimpango-0.1.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:5295d3a27d291ca0879b51cf1e398d129aa91db8c6b89c14417fa61cb1fa80e7"}, + {file = "manimpango-0.1.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6e0ed860825c4e57f16f06f1b14d9599217add991136edfb390f446c85fc4830"}, + {file = "manimpango-0.1.4-cp39-cp39-win32.whl", hash = "sha256:ec4497af4a23d6969dd045cc66695ae59d63157119908ce76bb5585450183b63"}, + {file = "manimpango-0.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:e165fd017864b7435da59f4a192f3f55354602e399e763ded20fbbd7fc5f9e42"}, + {file = "manimpango-0.1.4.tar.gz", hash = "sha256:693fbc52ea60c75acb9c98f23fc8e98a429b5ce02ab761ef9db24e9d61d62a49"}, +] markupsafe = [ {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, @@ -1185,6 +1145,10 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +networkx = [ + {file = "networkx-2.5-py3-none-any.whl", hash = "sha256:8c5812e9f798d37c50570d15c4a69d5710a18d77bafc903ee9c5fba7454c616c"}, + {file = "networkx-2.5.tar.gz", hash = "sha256:7978955423fbc9639c10498878be59caf99b44dc304c2286162fd24b458c1602"}, +] numpy = [ {file = "numpy-1.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949"}, {file = "numpy-1.19.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4"}, @@ -1222,22 +1186,13 @@ numpy = [ {file = "numpy-1.19.4.zip", hash = "sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512"}, ] packaging = [ - {file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"}, - {file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"}, -] -pangocairocffi = [ - {file = "pangocairocffi-0.4.0.tar.gz", hash = "sha256:ebebffb16861aca36823d39dfbcabe963eb4af03035ed9e0a701bd97a0e16af0"}, -] -pangocffi = [ - {file = "pangocffi-0.8.0.tar.gz", hash = "sha256:f8b03bc7dcc85e70f8cc58ca2b56b2077511a44c3d5c20bb0abdcbc66132288d"}, + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, ] pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] -pathtools = [ - {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, -] pillow = [ {file = "Pillow-8.0.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3"}, {file = "Pillow-8.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302"}, @@ -1296,8 +1251,8 @@ protobuf = [ {file = "protobuf-3.14.0.tar.gz", hash = "sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce"}, ] py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pycairo = [ {file = "pycairo-1.20.0-cp36-cp36m-win32.whl", hash = "sha256:e5a3433690c473e073a9917dc8f1fc7dc8b9af7b201bf372894b8ad70d960c6d"}, @@ -1310,17 +1265,13 @@ pycairo = [ {file = "pycairo-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:57166119e424d71eccdba6b318bd731bdabd17188e2ba10d4f315f7bf16ace3f"}, {file = "pycairo-1.20.0.tar.gz", hash = "sha256:5695a10cb7f9ae0d01f665b56602a845b0a8cb17e2123bfece10c2e58552468c"}, ] -pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, -] pydub = [ {file = "pydub-0.24.1-py2.py3-none-any.whl", hash = "sha256:25fdfbbfd4c69363006a27c7bd2346c4b886a0dd3da264c14d858b71a9593284"}, {file = "pydub-0.24.1.tar.gz", hash = "sha256:630c68bfff9bb27cbc5e1f02923f717c3bc5f4d73fd685fda08b6ce90f76dc69"}, ] pygments = [ - {file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"}, - {file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"}, + {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"}, + {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"}, ] pylint = [ {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, @@ -1331,20 +1282,20 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"}, - {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"}, + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, ] python-dateutil = [ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] pytz = [ - {file = "pytz-2020.4-py2.py3-none-any.whl", hash = "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"}, - {file = "pytz-2020.4.tar.gz", hash = "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268"}, + {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, + {file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, ] recommonmark = [ - {file = "recommonmark-0.6.0-py2.py3-none-any.whl", hash = "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852"}, - {file = "recommonmark-0.6.0.tar.gz", hash = "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb"}, + {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, + {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] regex = [ {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, @@ -1390,8 +1341,8 @@ regex = [ {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, ] requests = [ - {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, - {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] rich = [ {file = "rich-6.2.0-py3-none-any.whl", hash = "sha256:fa55d5d6ba9a0df1f1c95518891b57b13f1d45548a9a198a87b093fceee513ec"}, @@ -1433,8 +1384,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] sphinx = [ - {file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"}, - {file = "Sphinx-3.3.1.tar.gz", hash = "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300"}, + {file = "Sphinx-3.4.1-py3-none-any.whl", hash = "sha256:aeef652b14629431c82d3fe994ce39ead65b3fe87cf41b9a3714168ff8b83376"}, + {file = "Sphinx-3.4.1.tar.gz", hash = "sha256:e450cb205ff8924611085183bf1353da26802ae73d9251a8fcdf220a8f8712ef"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, @@ -1465,40 +1416,40 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tqdm = [ - {file = "tqdm-4.54.0-py2.py3-none-any.whl", hash = "sha256:9e7b8ab0ecbdbf0595adadd5f0ebbb9e69010e0bd48bbb0c15e550bf2a5292df"}, - {file = "tqdm-4.54.0.tar.gz", hash = "sha256:5c0d04e06ccc0da1bd3fa5ae4550effcce42fcad947b4a6cafa77bdc9b09ff22"}, + {file = "tqdm-4.55.0-py2.py3-none-any.whl", hash = "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416"}, + {file = "tqdm-4.55.0.tar.gz", hash = "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208"}, ] typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, - {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, - {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, + {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, + {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, + {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, + {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, + {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, + {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, + {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, + {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, + {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, ] typing-extensions = [ {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, @@ -1510,7 +1461,23 @@ urllib3 = [ {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, ] watchdog = [ - {file = "watchdog-0.10.4.tar.gz", hash = "sha256:e38bffc89b15bafe2a131f0e1c74924cf07dcec020c2e0a26cccd208831fcd43"}, + {file = "watchdog-1.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e2a531e71be7b5cc3499ae2d1494d51b6a26684bcc7c3146f63c810c00e8a3cc"}, + {file = "watchdog-1.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e7c73edef48f4ceeebb987317a67e0080e5c9228601ff67b3c4062fa020403c7"}, + {file = "watchdog-1.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e6574395aa6c1e14e0f030d9d7f35c2340a6cf95d5671354ce876ac3ffdd4d"}, + {file = "watchdog-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:27d9b4666938d5d40afdcdf2c751781e9ce36320788b70208d0f87f7401caf93"}, + {file = "watchdog-1.0.2-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f1ade0d0802503fda4340374d333408831cff23da66d7e711e279ba50fe6c4a"}, + {file = "watchdog-1.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f1d0e878fd69129d0d68b87cee5d9543f20d8018e82998efb79f7e412d42154a"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d948ad9ab9aba705f9836625b32e965b9ae607284811cd98334423f659ea537a"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:101532b8db506559e52a9b5d75a308729b3f68264d930670e6155c976d0e52a0"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:b1d723852ce90a14abf0ec0ca9e80689d9509ee4c9ee27163118d87b564a12ac"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:68744de2003a5ea2dfbb104f9a74192cf381334a9e2c0ed2bbe1581828d50b61"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:602dbd9498592eacc42e0632c19781c3df1728ef9cbab555fab6778effc29eeb"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:016b01495b9c55b5d4126ed8ae75d93ea0d99377084107c33162df52887cee18"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:5f1f3b65142175366ba94c64d8d4c8f4015825e0beaacee1c301823266b47b9b"}, + {file = "watchdog-1.0.2-py3-none-win32.whl", hash = "sha256:57f05e55aa603c3b053eed7e679f0a83873c540255b88d58c6223c7493833bac"}, + {file = "watchdog-1.0.2-py3-none-win_amd64.whl", hash = "sha256:f84146f7864339c8addf2c2b9903271df21d18d2c721e9a77f779493234a82b5"}, + {file = "watchdog-1.0.2-py3-none-win_ia64.whl", hash = "sha256:ee21aeebe6b3e51e4ba64564c94cee8dbe7438b9cb60f0bb350c4fa70d1b52c2"}, + {file = "watchdog-1.0.2.tar.gz", hash = "sha256:376cbc2a35c0392b0fe7ff16fbc1b303fd99d4dd9911ab5581ee9d69adc88982"}, ] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, diff --git a/pyproject.toml b/pyproject.toml index 8e98758011..cf5409f00f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "manim" -version = "0.1.1" +version = "0.2.0" description = "Animation engine for explanatory math videos." authors = ["The Manim Community Developers","3b1b <grant@3blue1brown.com>"] license="MIT" @@ -35,12 +35,11 @@ pydub = "*" pygments = "*" rich = "^6.0" pycairo = "^1.19" -pangocffi = "^0.8.0" -pangocairocffi = "^0.4.0" -cairocffi = "^1.1.0" -grpcio = { version = "*", optional = true } -grpcio-tools = { version = "*", optional = true } +manimpango = "^0.1.4" +grpcio = { version = "1.33.*", optional = true } +grpcio-tools = { version = "1.33.*", optional = true } watchdog = { version = "*", optional = true } +networkx = "^2.5" [tool.poetry.extras] js_renderer = ["grpcio","grpcio-tools","watchdog"] diff --git a/scripts/simple_cors_http_server.py b/scripts/simple_cors_http_server.py new file mode 100755 index 0000000000..2f3034193b --- /dev/null +++ b/scripts/simple_cors_http_server.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +from http.server import HTTPServer, SimpleHTTPRequestHandler, test +import sys + + +class CORSRequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Access-Control-Allow-Origin", "*") + SimpleHTTPRequestHandler.end_headers(self) + + +if __name__ == "__main__": + test( + CORSRequestHandler, + HTTPServer, + port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000, + ) diff --git a/manim/grpc/update_protos.sh b/scripts/update_protos.py similarity index 53% rename from manim/grpc/update_protos.sh rename to scripts/update_protos.py index d6b43b68c5..e0b52d984e 100755 --- a/manim/grpc/update_protos.sh +++ b/scripts/update_protos.py @@ -1,9 +1,17 @@ -#!/bin/sh +#!/usr/bin/env python +""" +This is intended to be run from manim/grpc +""" -python \ +import os + +CMD_STRING = """ +poetry run python \ -m grpc_tools.protoc \ -I./proto \ --python_out=./gen \ --grpc_python_out=./gen \ ./proto/frameserver.proto \ ./proto/renderserver.proto +""" +os.system(CMD_STRING) diff --git a/tests/conftest.py b/tests/conftest.py index 32deec198d..abef7f47d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ def pytest_addoption(parser): "--skip_slow", action="store_true", default=False, - help="Will skip all the slow marked tests. Slow tests are arbitrarly marked as such.", + help="Will skip all the slow marked tests. Slow tests are arbitrarily marked as such.", ) parser.addoption( "--show_diff", diff --git a/tests/control_data/graphical_units_data/geometry/ElbowTest.npz b/tests/control_data/graphical_units_data/geometry/ElbowTest.npz new file mode 100644 index 0000000000000000000000000000000000000000..fb9aa2d245dc9bde10ae31ea875c84f6501c0f14 GIT binary patch literal 1912 zcmWIWW@Zs#U|`??Vnqh^U8dg8*cccZSS1<w7(^J-iV|~E<5Ln#67}*5Dj69Bz%oFk z3?Nk?@b;FW*O35$hKDz1dGC3rxx_%F@hI~RO*64&6Cb$gq=XA?4fKE5Si1M%y5KiW z_YZ{cS-I@*!`j%pF>&|hMDveU$Mt>wJLP=vzlycRQ$HQ9s+!q$^=?&E-~Qj-4>RS= zfB!73kTZWjxvVg3sq6Kym9}dyrQfRU>t6H&YCXgeqvU7^4CfGN_`f7`*4(M7JHPK{ zWMJSs8o%uIuD$np85jyQN<<kL9L{X3|Nm>>^-y3KFqX!?)&gc=hFDex27$$^fkH`O zH9hN?7#KP(g47u9;$mP>a0>^Tb>sy|u>?r*C>RZa(Gb8A0y&S_1H2iTM3`~alL$Qx bz%0dpR%-@$vw|xtMg~R(8=$NjJ4g)xTb6o< literal 0 HcmV?d00001 diff --git a/tests/control_data/graphical_units_data/geometry/ZIndexTest.npz b/tests/control_data/graphical_units_data/geometry/ZIndexTest.npz new file mode 100644 index 0000000000000000000000000000000000000000..381e43b5ba90902c2253a44e8285312f07b8286c GIT binary patch literal 7501 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 literal 0 HcmV?d00001 diff --git a/tests/control_data/graphical_units_data/plot/AxesTest.npz b/tests/control_data/graphical_units_data/plot/AxesTest.npz new file mode 100644 index 0000000000000000000000000000000000000000..51d28b7d64340c0a3c1112ecdd21fd0ef96abd94 GIT binary patch literal 4871 zcmeHLdsLH07XK72tqSNCWmO8>2kL4eR*DD`aaBNt5)=XjgxDgYvLrkL7$Af#+tMOP zDFWpgm1hYd<q-mbBv@8KEKNxakyj!@h#?7)S0I6qP7qt$p0@w&p0nrJFDJ=&X71d% z_jm7|`OSE^>a8(`An0ZAeI3f(r;tYf1%i^+ZiNgXW9WS32Vudeb01(outJC9RzVw= zdw?jUlhQr%A<U;p5wk6V=GSlG%I#dbyLS`UKtkb7zHwvU8Vl>8CAV|OHqE&Weif7z z<M}8iKGVzNA9pM)9wVLdKQ})1(#83kj%yeaquJeMN4hqq#+~}Nx^TL>UC<Qr?TC_A zLCr}tn{SIGGde;Vzb)3x`o_z(=BC@`*`o9lq@5a!5S3y+PeJ0fJ>0n`$+^mIg`_1O z%hOiVrObmaKAPwQ3f5Q+3%++TLu0f!7cgg}9buciJ%7()I({kg_D3rC`XziKTRV$A zv=qiEJ@Lv@ub%zx;>-H#x$wEcEN2%Nu5U=uYzpn*?0wJm!E6%<x_)wuO(Zs=hw`_; z0t?OJWwkTyMvkS;QR1sDLup&BIdeh+@sMVQ%MovYAdk&E3&W4}*9N)dTZgz$iU-gw zmZI7ZZ>L7k0viA68zGa^gjRR~m>OA|pqgs9#H;Eh67kBun?)0hIY9&sua;C?2{MAu zcu1ySf}qOORwee%))3=?tept=mRN<mzM<)HI2=BLUYP7KHwmDI4}ZfE8|Y@%w>_Om zK$<Clo~8j&C{i{+gK4Jj!EQGv2eY>WPg08sODe|%46m*2aKzgv;Y}HCH3WH`3QX>A ziN*A@2-?>v^#ahJZxecwxCep`Z?^PnsC--wT-1KvQJ^5x$E!#yJwpVo#>o~Aa2mPV zU1(Q-8N*{cY75KVi~Q7QRzXnJ;~p1tO}mt2WN5ghLl7q=bHo664`h3LyVJq+t5+N4 zY^kS8orJ3nWYA}pE#Y0PD9TaF`m$nV4>HT$@=nVN-U)J!Q;Bl93YIny%ggo&^i@0P zv{zFou&}<mviFJoO%Xx;4HrG>lXtdXVZX8@sUWt})<Mw2UGK7u5+r50CyDEUE65?7 zxahX}{>LVP2PPiqn<scw`&hpJ_4TJSpF$uIlfG@Z>NCxC@&^E)T0;9L24(D50c=%P zEwZ{9DTP%oAD~rm5snP1wlM0u6S`;&G={lyD-bYzu`Z<96sO6F7Df4<nx>mnMSf__ ziIy1oNhzPg_X6E6XMt;yyE}<)1-2M>3Tr7YZY(c^<75)xrEYzejL-c)yljn|8a>iC z4caP6H*VZmc`b1+$vmVQ&6-h(iT?E<f(O>&imL3YeE}}m@@8+IX>T<cVjuOyFWUx0 zG3mRs08-ugwGi}u-cd5rcX`M@LW6g3w+^OGQ#83Ne=`KN4Qoi$-J#xKY)b4~p~z(| zhVtY5s%6!temhs%`lIPj$6g=mseY<&zM^DhjwguBr%Ku;tyaVi2%>|?D+YEHpvg#^ z6rEiUFV|xwoEe@E>b2hQiVkvZg$bUW1KM$=Dl|uf2hMoi73HD?ei>Z@!EAjH2s9R0 zc{r^Gxcq4k5}5f89XRkeXCEN@pU1m(o^CKOp!(LIe~(0(Qa%`2Dd27C&g0n8g==sO z5UKiJH!`CBeD|gLb9cY+J}Q#ecHAq50nNN-Gyv&$C-`dskl0v40xCZILu+0nVSBDn zC_I1eTqzHUi<eE-i9yYISfo3@8Erp>?<%&{Rp4@nngjyD{^aCj@+VybK^eJllrAPw zLm-ezFA`q=wd}!`vskR|&d%e1nDPt;BkU!8eSd>&T89}do(XaoDW-#x`gI+>kdfHI z0ZUPlZrf2iK+ujaYF6*yq?jju88b1MH^E$(csF|dK1Zwv?cIdkyf-t_$0mij8oG+4 zulw@2er)!S?<wO|jwKX#dr2u)h@UG<-)>uO>ECEfK$P_Cy4qb}3nTl}tQb>WdnC=` zVpkB9GDr8r)U>oTW47mlQoaXoHH7VWEw-UJxdK59R433y<pt<VcvO7|+X{bZC{j-4 zH!>+vY&dO3g>_9-L`R(ozqPiEUEz;rX_UQl*|dAb2-+y2WUNyg6AYTC*j`biL~5Ow zYpJsP+HNW;sBc=5vv`nXH>Icpt?#L*&SolV$1pm!sV2XYsbenv^ypB!YOLT&4&Vab zYQQ46>RG8L3WbUj7sYb&LW=X;?L^la@C(YBp;+N-o)lQpfdC|oUKNN%G9FjO^&n@t z-8JJeDADX!t}sSJnDAAEL#9$8BpzCj)%PmOsiFKW1jWDf^i!3)7v5Q0b<~G<rKZZ{ znGTKHF`wuX3c6X8(-1d19<eZuBj8%+A6+5iZ3n}Ts+9v6F5qEj)FZEWJwFc2t<LML zNu5CklJf1M#+o!>i{8T=Bhv(a>L7jW*k~5QN}#tC(iJ+08;zcDTu>Ar&v2MJF13pp z{^nyHjAjW>ZuQPl!QR9HknQoVzJN#Z17ZUt0JY)a;YL0_m@U2_im`w2iNSV1p^v~U zzE!3&BPVNxZ)gXy;Y9RZN6+p|LT{HY4&t`4PImfo92C`KoveGH)mFsch8yLZrL+vW znB%ec19+xIGdw^h@5l=raFIGT7Qqw_Hb=P<(qG1&@44wTEZ)?<cSHN$H2aLj#By|c z3Ec6?qbo402_hxnlu~;1dKA5Dxqk%jb%W$xbH;bjk#aXzqu%(qa2jgqz$MK-a%j60 zWp<WfXY_Soq5iy>sDF7ErghSJGCBu_O6=r^svK_Bp=?+49?>_?{_^X|lX|Dw1b1DV z8Od<mf>Oxp*=Dg0g#F=5Z<FfG5!d~GjQUN|xRVbLv6)iOJbUY-ncF6)QcwboN*n%> zIa#0egmQYU+m*sl`bjb@JmAmg3m93EN?bqaAcPTi@NO-{$osRIOKNH3fP2AyO9seB z$>KB{rm0<1<?@SU!Pb-L5lobFek8+0yl5HNdZ6g?qt7nN`>D4>)`-PP!o&qjgAE&u zY4<F%`CsK&`g^NKUWQD!Pc3v6yot$$n?HOI*;F&`jj$0>tnKad_lNag+m8_x*07e; zy|u1Z&2KTDe6wBx)`$0`y<}HV0)dhY11MbfJ+keXy_ZjPO9cdVm6MV_!-o4v{ti8S zzYM%zYdMKn1$Ed5taU3*=>BwZ>*-*&9(48%iH@qgkW1{(h}(7RXFF2Bb2}2}!n>y{ z-V|2IaC(3Ne!NWfqzVPbdfz_IrOH;g@^ara;9!;Zd@|U%euD@83GVZ(_*ZCz#Kp%C zY%nxzr0>RLzyp;Dx)W7t&xKNV7bc=_`P{i~ndtJLkvfl`NL_S|KM?<u1Fsx8eg*FD zo=?8bw;^?ZVd3icM=8I3+qO&KjFSI9I-j_K6?Imr+%gr@Hw~is|Mpa7vGhD#SFJYI i`>kuMpDa#Vw*TjsSRSrx!Q}}AtpXpw<sj<vMb;12Lq5X* literal 0 HcmV?d00001 diff --git a/tests/control_data/graphical_units_data/plot/PlotFunctions.npz b/tests/control_data/graphical_units_data/plot/PlotFunctions.npz new file mode 100644 index 0000000000000000000000000000000000000000..dfddd05f2819a38f4d5c0d3afcfa345649d11c36 GIT binary patch literal 14719 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 literal 0 HcmV?d00001 diff --git a/tests/control_data/graphical_units_data/transform/AnimationBuilderTest.npz b/tests/control_data/graphical_units_data/transform/AnimationBuilderTest.npz new file mode 100644 index 0000000000000000000000000000000000000000..04880d7cc215274bdae44c0b5addbd6bc92bbbf2 GIT binary patch literal 2914 zcmeH}jZafo7>5sEvZzB`z%kswxlQGw#AQ|ERA@9ChEmIDVJZ?vb<nc5n9BS>9VLs- zFg~T|)}jT3i=|d^vdapnY_Z7Ph;0GI`hgBbVG@BdT~TD`J@+O{mi-5Mo3`n5pT6(! zygj#VoWRRF03q}WzQ2tkO;gU>rXr;Ac?0>O0F<UqReqkbB{eHGLbY|52l6Lpu!_Kf zW5^lx#lnQ3);m4pVGi@Ouz<O<7smUnu(oNci*Fa)Slx2)c)@tsv;5TS-oGrJ9h9kF zK5yBtY3bY3`=9OhTWM|SY_SHozIHuo?9FlAoMCwQ%;LIwx8s^sbiee}_W3!JBi1dM zJ4Rj2uHMU{oQaW}ZvB~N_to0-o664`Qd(9`3_YB1r9bsGJQ@nwV>#d09Ch>hM0(>> zcUF3Otp^+N<>L!Dfr_Zkfrjrp((F(0;@j=X;*R^S{=vbRB)loI0(8`yN*}z5Z}rg| zBMx^+Pumi)wn#Xyw10GT^xQF3W~RQhw6uSCxZZ*fLvl*WGGAX``x9@3*4hQaz+3Wg ze3e{1dqtAVcp|iOgPxw+5QtFg#U@@tg&7x%?5Q?-LX$2PcP`EpsiU9~UpcO*<3o}k zgQ{3bt7>Np#MQmeN%A&MsySe1yQp73!`gSLeXL!q*K&45JZBpP7FMdJzTqTYIVVwS zPC9+>23t}ZR>|7==4#eneCRbjN!e<PQL!o47&&RoOtX^ha4;t^8JwhdNM^tUY-M!z zI#`=hUe^tvzL4H10$`HWJq9qWr3Z<TOFPS9dc1DJy9DCDqOr)|%S)I9v1^U^RRY|+ z?qL90fw+KxUA*qU30Nbx9>dd~lcS>|Vc2emWB^8794ageA;2aXZ~%xDmJwsXA#oEU zL|7II#(+)Y2E*S#Uozl0nw`9a<xo6dBrbz)tr%DUOveCL(z=zGunNGKNZd{G<nR*C z0#Jy=l{n64+jM#G`SN$@@@j!M)Ed`+1n<n&<$Vd@1Z|WF0ATYZfR#3h0yd|HmhN^= zMH-!)8n42oj+f9?nFPqF{RHIFQW=1Qa;go$f)ZM)AV5Y*?RX(tzIdh{FJw--E>9xE z`&3<RTw4HOJ_fLH)r*g;)4TV4HQC#=DMH`Z*Y{w>&zqy8qw$6C)8xmePM*|VQ_R{s zAMfkfPfI>b_Cb8V8(!}w6g~2|wWMsf2BGTMq>^AE=7kM`Mw<koyRk_#^|U9d@OI{{ z=i&9dXHU!*iak)=T&0x-or%*4@KRdiz*u5W93$Wz<wq$5bS5eU_$?Ynx=n|FUP5Hy zO%7apxSXk4gqtCgJp76sgZiV4G!Vd$Jp3pawj<13NKwCBxetfX49QK|atfgbMNC~0 zfae&<FbFDSC<A~ya+4a2pdyAM#s~&r9EeW3C&D54dz@4Xq;OQ4M~@lt7F*?{LJ6ob zJ8A*!byCRyMk~z@VnjKqw+N^)|Ivx#6h|ooNpZEzGy>fg6fwapkeluhU|@m^Ay2l< z^c=wPB4&^n*>ckm0Dh8k9gc%?nL+X_lA9jGu<G-ejgS-lDKm8gupMHuiIFTf`9g{w zEd#S@eVm~RhhdG?v=PAX&B!2Ndd+^;z!y3J`85|<&=bQN7vfhZL5Mj_`SloRW^6xg zhGxaOawiKeRkEN-%NmxooT1mUh9!nGn!i)l;IV1qFUUwYZQAc_9%CB^RP8KSw4ViC z^ko*9$2p+V6M#Y$jzn1F?aj$-mzEPkk`%s@$(f2HhuJe(AFzA9_mki5{jg!-tjT5F zilyJ43&o@L3u!FF!$&=_Z25XiGg}^ieZM?jzzO(Y%!w0tcm{X{;Kl#nKjMF9qQO@K X`p^I4IDyX;pdR?P5<mM-gIE6m;T^}H literal 0 HcmV?d00001 diff --git a/tests/control_data/graphical_units_data/updaters/UpdateSceneDuringAnimationTest.npz b/tests/control_data/graphical_units_data/updaters/UpdateSceneDuringAnimationTest.npz new file mode 100644 index 0000000000000000000000000000000000000000..59e78df0241bb97e96541e81555e3ccdc3fd9857 GIT binary patch literal 6255 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 literal 0 HcmV?d00001 diff --git a/tests/helpers/video_utils.py b/tests/helpers/video_utils.py index c051c0604a..13ee05850f 100644 --- a/tests/helpers/video_utils.py +++ b/tests/helpers/video_utils.py @@ -38,7 +38,7 @@ def get_config_from_video(path_to_video): def save_control_data_from_video(path_to_video, name): """Helper used to set up a new test that will compare videos. This will create a new .json file in control_data/videos_data that contains - informations tested of the video, including its hash. Refer to the wiki for more informations. + information tested of the video, including its hash. Refer to the wiki for more informations. Parameters ---------- diff --git a/tests/test_config.py b/tests/test_config.py index f18880d095..a57baf3c3a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -64,7 +64,7 @@ def test_background_color(): def test_digest_file(tmp_path): - """Test that a config file can be digested programatically.""" + """Test that a config file can be digested programmatically.""" with tempconfig({}): tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) tmp_cfg.write( diff --git a/tests/test_graph.py b/tests/test_graph.py new file mode 100644 index 0000000000..d7052bb2de --- /dev/null +++ b/tests/test_graph.py @@ -0,0 +1,11 @@ +from manim import Graph + + +def test_graph_creation(): + vertices = [1, 2, 3, 4] + edges = [(1, 2), (2, 3), (3, 4), (4, 1)] + layout = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]} + G_manual = Graph(vertices=vertices, edges=edges, layout=layout) + assert str(G_manual) == "Graph on 4 vertices and 4 edges" + G_spring = Graph(vertices=vertices, edges=edges) + assert str(G_spring) == "Graph on 4 vertices and 4 edges" diff --git a/tests/test_graphical_units/test_axes.py b/tests/test_graphical_units/test_axes.py index 6364b89891..f3d1f8f416 100644 --- a/tests/test_graphical_units/test_axes.py +++ b/tests/test_graphical_units/test_axes.py @@ -30,7 +30,7 @@ def construct(self): self.setup_axes(animate=True) -MODULE_NAME = "graph" +MODULE_NAME = "plot" @pytest.mark.slow diff --git a/tests/test_graphical_units/test_creation.py b/tests/test_graphical_units/test_creation.py index c7c1e1bed5..240764a795 100644 --- a/tests/test_graphical_units/test_creation.py +++ b/tests/test_graphical_units/test_creation.py @@ -46,12 +46,6 @@ def construct(self): self.play(FadeInFrom(square, direction=UP)) -class FadeInFromDownTest(Scene): - def construct(self): - square = Square() - self.play(FadeInFromDown(square)) - - class FadeOutAndShiftTest(Scene): def construct(self): square = Square() diff --git a/tests/test_graphical_units/test_geometry.py b/tests/test_graphical_units/test_geometry.py index cee3eef72a..081b9e7a86 100644 --- a/tests/test_graphical_units/test_geometry.py +++ b/tests/test_graphical_units/test_geometry.py @@ -90,7 +90,7 @@ def construct(self): self.play(Animation(a)) -class Elbowtest(Scene): +class ElbowTest(Scene): def construct(self): a = Elbow() self.play(Animation(a)) @@ -135,6 +135,21 @@ def construct(self): self.wait() +class ZIndexTest(Scene): + def construct(self): + circle = Circle().set_fill(RED, opacity=1) + square = Square(side_length=1.7).set_fill(BLUE, opacity=1) + triangle = Triangle().set_fill(GREEN, opacity=1) + square.z_index = 0 + triangle.z_index = 1 + circle.z_index = 2 + + self.play(FadeIn(VGroup(circle, square, triangle))) + self.play(ApplyMethod(circle.shift, UP)) + self.play(ApplyMethod(triangle.shift, 2 * UP)) + self.wait(1) + + MODULE_NAME = "geometry" diff --git a/tests/test_graphical_units/test_graph.py b/tests/test_graphical_units/test_graphscene.py similarity index 97% rename from tests/test_graphical_units/test_graph.py rename to tests/test_graphical_units/test_graphscene.py index 67e302e8c2..a09a33ac4f 100644 --- a/tests/test_graphical_units/test_graph.py +++ b/tests/test_graphical_units/test_graphscene.py @@ -29,7 +29,7 @@ def construct(self): self.play(Animation(f)) -MODULE_NAME = "graph" +MODULE_NAME = "plot" @pytest.mark.slow diff --git a/tests/test_graphical_units/test_movements.py b/tests/test_graphical_units/test_movements.py index b024f7bc1d..163ca21523 100644 --- a/tests/test_graphical_units/test_movements.py +++ b/tests/test_graphical_units/test_movements.py @@ -43,13 +43,13 @@ def construct(self): class MoveToTest(Scene): def construct(self): square = Square() - self.play(square.move_to, np.array([1.0, 1.0, 0.0])) + self.play(square.animate.move_to(np.array([1.0, 1.0, 0.0]))) class ShiftTest(Scene): def construct(self): square = Square() - self.play(square.shift, UP) + self.play(square.animate.shift(UP)) MODULE_NAME = "movements" diff --git a/tests/test_graphical_units/test_transform.py b/tests/test_graphical_units/test_transform.py index 84a6c4285b..2e058d9757 100644 --- a/tests/test_graphical_units/test_transform.py +++ b/tests/test_graphical_units/test_transform.py @@ -75,7 +75,7 @@ def construct(self): circle = Circle() self.play(Transform(square, circle)) square.save_state() - self.play(square.shift, UP) + self.play(square.animate.shift(UP)) self.play(Restore(square)) @@ -131,6 +131,11 @@ def construct(self): self.play(FadeOut(square)) +class AnimationBuilderTest(Scene): + def construct(self): + self.play(Square().animate.shift(RIGHT).rotate(PI / 4)) + + MODULE_NAME = "transform" diff --git a/tests/test_graphical_units/test_updaters.py b/tests/test_graphical_units/test_updaters.py index 44c0722e70..addb950468 100644 --- a/tests/test_graphical_units/test_updaters.py +++ b/tests/test_graphical_units/test_updaters.py @@ -12,7 +12,7 @@ def construct(self): self.add(dot, square) square.add_updater(lambda m: m.next_to(dot, RIGHT, buff=SMALL_BUFF)) self.add(square) - self.play(dot.shift, UP * 2) + self.play(dot.animate.shift(UP * 2)) square.clear_updaters() @@ -24,6 +24,15 @@ def construct(self): line_2.rotate(theta.get_value(), about_point=ORIGIN) +class UpdateSceneDuringAnimationTest(Scene): + def construct(self): + def f(mob): + self.add(Square()) + + s = Circle().add_updater(f) + self.play(ShowCreation(s)) + + MODULE_NAME = "updaters" diff --git a/tests/utils/GraphicalUnitTester.py b/tests/utils/GraphicalUnitTester.py index 3c191320ac..5dd9e6ff43 100644 --- a/tests/utils/GraphicalUnitTester.py +++ b/tests/utils/GraphicalUnitTester.py @@ -76,12 +76,12 @@ def _load_data(self): The pre-rendered frame. """ frame_data_path = os.path.join( - os.path.join(self.path_control_data, "{}.npz".format(str(self.scene))) + os.path.join(self.path_control_data, f"{self.scene}.npz") ) return np.load(frame_data_path)["frame_data"] def _show_diff_helper(self, frame_data, expected_frame_data): - """Will visually display with matplotlib differences between frame generared and the one expected.""" + """Will visually display with matplotlib differences between frame generated and the one expected.""" import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec diff --git a/tests/utils/logging_tester.py b/tests/utils/logging_tester.py index e2dc642a48..73fb88eec5 100644 --- a/tests/utils/logging_tester.py +++ b/tests/utils/logging_tester.py @@ -46,7 +46,7 @@ def logs_comparison(control_data_file, log_path_from_media_dir): Parameters ---------- control_data_file : :class:`str` - Name of the control data file, i.e. .log that will be compared to the outputed logs. + Name of the control data file, i.e. .log that will be compared to the outputted logs. .. warning:: You don't have to pass the path here. .. example:: "SquareToCircleWithLFlag.log" @@ -68,7 +68,7 @@ def wrapper(*args, **kwargs): tests_directory = os.path.dirname( os.path.dirname(os.path.abspath(__file__)) ) - controle_data_path = os.path.join( + control_data_path = os.path.join( tests_directory, "control_data", "logs_data", control_data_file ) path_log_generated = tmp_path / log_path_from_media_dir @@ -80,7 +80,7 @@ def wrapper(*args, **kwargs): False ), f"'{parent.name}' does not exist in '{parent.parent}' (which exists). " break - _check_logs(controle_data_path, str(path_log_generated)) + _check_logs(control_data_path, str(path_log_generated)) return result return wrapper