Skip to content

New Feature: IPython %%manim magic #943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ manim example.py SquareToCircle -p -ql
You should see your native video player program pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this
[GitHub repository](master/example_scenes). You can also visit the [official gallery](https://docs.manim.community/en/latest/examples.html) for more advanced examples.

Manim also ships with a `%%manim` IPython magic which allows to use it conveniently in JupyterLab (as well as classic Jupyter) notebooks. See the
[corresponding documentation](https://docs.manim.community/en/latest/reference/manim.utils.ipython_magic.ManimMagic.html) for some guidance.

## Command line arguments

The general usage of Manim is as follows:
Expand Down
3 changes: 2 additions & 1 deletion docs/rtd-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
imageio-ffmpeg
imageio-ffmpeg
jupyterlab
1 change: 1 addition & 0 deletions docs/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Utilities
~utils.color
~utils.config_ops
~utils.hashing
~utils.ipython_magic
~utils.images
~utils.iterables
~utils.paths
Expand Down
7 changes: 7 additions & 0 deletions docs/source/tutorials/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ Every file containing code that produces a video with manim will be stored
here, as well as any output files that manim produces and configuration files
that manim needs.

.. note::

In case you like to work with Jupyterlab / Jupyter notebooks, there is good news:
Manim ships with a ``%%manim`` IPython magic command which makes it easy to use
in such a setting as well. Find out more in the
:meth:`corresponding documentation <manim.utils.ipython_magic.ManimMagic.manim>`.


Your first Scene
****************
Expand Down
10 changes: 10 additions & 0 deletions manim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@
from .utils.tex_templates import *
from .utils import unit

try:
from IPython import get_ipython
from .utils.ipython_magic import ManimMagic
except ImportError:
pass
else:
ipy = get_ipython()
if ipy is not None:
ipy.register_magics(ManimMagic)

from .plugins import *

try:
Expand Down
2 changes: 1 addition & 1 deletion manim/animation/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def __init__(

def _typecheck_input(self, mobject: Mobject) -> None:
if mobject is None:
logger.warning("creating dummy animation")
logger.debug("creating dummy animation")
elif not isinstance(mobject, Mobject):
raise TypeError("Animation only works on Mobjects")

Expand Down
1 change: 1 addition & 0 deletions manim/scene/scene_file_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,4 +545,5 @@ def flush_cache_directory(self):

def print_file_ready_message(self, file_path):
"""Prints the "File Ready" message to STDOUT."""
config["output_file"] = file_path
logger.info("\nFile ready at %(file_path)s\n", {"file_path": file_path})
120 changes: 120 additions & 0 deletions manim/utils/ipython_magic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Utilities for using Manim with IPython (in particular: Jupyter notebooks)"""

import hashlib
import mimetypes
import os
import shutil
from pathlib import Path

from manim import config, tempconfig

from .._config.main_utils import parse_args

try:
from IPython import get_ipython
from IPython.core.magic import (
Magics,
magics_class,
line_cell_magic,
needs_local_scope,
)
from IPython.display import display, Image, Video
except ImportError:
pass
else:

@magics_class
class ManimMagic(Magics):
def __init__(self, shell):
super(ManimMagic, self).__init__(shell)
self.rendered_files = dict()

@needs_local_scope
@line_cell_magic
def manim(self, line, cell=None, local_ns=None):
r"""Render Manim scenes contained in IPython cells.
Works as a line or cell magic.

.. note::

This line and cell magic works best when used in a JupyterLab
environment: while all of the functionality is available for
classic Jupyter notebooks as well, it is possible that videos
sometimes don't update on repeated execution of the same cell
if the scene name stays the same.

This problem does not occur when using JupyterLab.

Please refer to `<https://jupyter.org/>`_ for more information about JupyterLab
and Jupyter notebooks.

Usage in line mode::

%manim MyAwesomeScene [CLI options]

Usage in cell mode::

%%manim MyAwesomeScene [CLI options]

class MyAweseomeScene(Scene):
def construct(self):
...

Run ``%manim -h`` for possible command line interface options.
"""
if cell:
exec(cell, local_ns)

cli_args = ["manim", ""] + line.split()
if len(cli_args) == 2:
# empty line.split(): no commands have been passed, call with -h
cli_args.append("-h")

try:
args = parse_args(cli_args)
except SystemExit:
return # probably manim -h was called, process ended preemptively

with tempconfig(local_ns.get("config", {})):
config.digest_args(args)

exec(f"{config['scene_names'][0]}().render()", local_ns)
local_path = Path(config["output_file"]).relative_to(Path.cwd())
tmpfile = (
Path(config["media_dir"])
/ "jupyter"
/ f"{_video_hash(local_path)}{local_path.suffix}"
)

if local_path in self.rendered_files:
self.rendered_files[local_path].unlink()
self.rendered_files[local_path] = tmpfile
os.makedirs(tmpfile.parent, exist_ok=True)
shutil.copy(local_path, tmpfile)

file_type = mimetypes.guess_type(config["output_file"])[0]
if file_type.startswith("image"):
display(Image(filename=config["output_file"]))
return

# videos need to be embedded when running in google colab
video_embed = "google.colab" in str(get_ipython())

display(
Video(
tmpfile,
html_attributes='controls autoplay loop style="max-width: 100%;"',
embed=video_embed,
)
)


def _video_hash(path):
sha1 = hashlib.sha1()
with open(path, "rb") as f:
while True:
data = f.read(65536)
if not data:
break
sha1.update(data)
return sha1.hexdigest()
Loading