-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
60004a5
commit ca29979
Showing
6 changed files
with
277 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,70 @@ | ||
# jupyter-manim | ||
%%mainm cell magic for IPython/Jupyter to show the output video | ||
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://choosealicense.com/licenses/mit/) | ||
|
||
|
||
Integrates [manim](https://github.com/3b1b/manim) (animation engine for explanatory math videos) | ||
with Jupyter displaying the resulting video when using `%%manim` cell magic to wrap a scene definition. | ||
|
||
### Quick preview | ||
|
||
<img src='screenshots/cell_magic_demo.png'> | ||
|
||
The code in the example above comes from the excellent [manim tutorial](https://github.com/malhotra5/Manim-Tutorial). | ||
|
||
### Installation | ||
|
||
```sh | ||
pip3 install jupyter-manim | ||
``` | ||
|
||
### Usage | ||
|
||
Your arguments will be passed to manim, exactly as if these were command line options. | ||
|
||
For example, to render scene defined with class `Shapes(Scene)` use | ||
|
||
```python | ||
%%manim Shapes | ||
from manimlib.scene.scene import Scene | ||
from manimlib.mobject.geometry import Circle | ||
from manimlib.animation.creation import ShowCreation | ||
|
||
class Shapes(Scene): | ||
|
||
def construct(self): | ||
circle = Circle() | ||
self.play(ShowCreation(circle)) | ||
``` | ||
|
||
NOTE: currently the code has to be self-contained as it will be run in a separate namespace. | ||
Thus, all the imports have to be contained in your cell. | ||
|
||
In future, an option to export the current namespace (or specific variables) will be added. | ||
It could be implemented by pickling the Python locals and globals and then pre-pending the cell with an un-pickling script (PRs welcome!). | ||
|
||
In the latest version of manimlib (not yet released) you will be able to import everything at once using: | ||
|
||
```python | ||
from manimlib.imports import * | ||
``` | ||
|
||
|
||
To display manim help and options use: | ||
|
||
``` | ||
%%manim -h | ||
pass | ||
``` | ||
|
||
|
||
|
||
The `%%manim` magic (by default) hides the progress bars as well as other logging messages generated by manim. | ||
You can disable this behaviour using `--verbose` flag | ||
|
||
#### Video player control options | ||
|
||
- `--no-control` - hides the controls | ||
- `--no-autoplay` - disables the autoplay feature | ||
- `-r` or `--resolution` - control the height and width of the video player; | ||
this option is shared with manim and requires the resolution in following format: | ||
`height,width`, e.g. `%%manim Shapes -r 200,1000` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
from IPython.core.magic import Magics, magics_class, cell_magic | ||
from unittest.mock import patch | ||
from tempfile import NamedTemporaryFile | ||
import manimlib | ||
from IPython.display import HTML | ||
import sys | ||
from io import StringIO | ||
from contextlib import ExitStack, suppress, redirect_stdout, redirect_stderr | ||
from warnings import warn | ||
from IPython import get_ipython | ||
from pathlib import Path | ||
|
||
std_out = sys.stdout | ||
|
||
|
||
def video(path, width=854, height=480, controls=True, autoplay=True): | ||
return HTML(f""" | ||
<video | ||
width="{width}" | ||
height="{height}" | ||
autoplay="{'autoplay' if autoplay else ''}" | ||
{'controls' if controls else ''} | ||
> | ||
<source src="{path}" type="video/mp4"> | ||
</video> | ||
""") | ||
|
||
|
||
class StringIOWithCallback(StringIO): | ||
|
||
def __init__(self, callback, **kwargs): | ||
super().__init__(**kwargs) | ||
self.callback = callback | ||
|
||
def write(self, s): | ||
super().write(s) | ||
self.callback(s) | ||
|
||
|
||
@magics_class | ||
class ManimMagics(Magics): | ||
path_line_start = 'File ready at ' | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.defaults = { | ||
'autoplay': True, | ||
'controls': True, | ||
'silent': True, | ||
'width': 854, | ||
'height': 480 | ||
} | ||
|
||
video_settings = {'width', 'height', 'controls', 'autoplay'} | ||
magic_off_switches = { | ||
'verbose': 'silent', | ||
'no-controls': 'controls', | ||
'no-autoplay': 'autoplay' | ||
} | ||
|
||
@cell_magic | ||
def manim(self, line, cell): | ||
# execute the code - won't generate any video, however it will introduce | ||
# the variables into the notebook's namespace (enabling autocompletion etc); | ||
# this also allows users to get feedback on some code errors early on | ||
get_ipython().ex(cell) | ||
|
||
user_args = line.split(' ') | ||
|
||
# path of the output video | ||
path = None | ||
|
||
settings = self.defaults.copy() | ||
|
||
# disable the switches as indicated by the user | ||
for key, arg in self.magic_off_switches.items(): | ||
if '--' + key in user_args: | ||
user_args.remove('--' + key) | ||
settings[arg] = False | ||
|
||
resolution_index = ( | ||
user_args.index('-r') if '-r' in user_args else | ||
user_args.index('--resolution') if '--resolution' in user_args else | ||
None | ||
) | ||
if resolution_index is not None: | ||
# the resolution is passed as "height,width" | ||
try: | ||
h, w = user_args[resolution_index + 1].split(',') | ||
settings['height'] = h | ||
settings['width'] = w | ||
except (IndexError, KeyError): | ||
warn('Unable to retrieve dimensions from your resolution setting, falling back to the defaults') | ||
|
||
silent = settings['silent'] | ||
|
||
def catch_path_and_forward(lines): | ||
nonlocal path | ||
for line in lines.split('\n'): | ||
if not silent: | ||
print(line, file=std_out) | ||
|
||
if line.startswith(self.path_line_start): | ||
path = line[len(self.path_line_start):].strip() | ||
|
||
with NamedTemporaryFile('w', suffix='.py') as f: | ||
f.write(cell) | ||
f.flush() | ||
|
||
args = ['manim', f.name, *user_args] | ||
|
||
stdout = StringIOWithCallback(catch_path_and_forward) | ||
|
||
with ExitStack() as stack: | ||
|
||
enter = stack.enter_context | ||
|
||
enter(patch.object(sys, 'argv', args)) | ||
enter(suppress(SystemExit)) | ||
enter(redirect_stdout(stdout)) | ||
|
||
if silent: | ||
stderr = StringIO() | ||
enter(redirect_stderr(stderr)) | ||
|
||
manimlib.main() | ||
|
||
if path: | ||
path = Path(path) | ||
assert path.exists() | ||
|
||
# To display a video in Jupyter, we need to have access to it | ||
# so it has to be within the working tree. The absolute paths | ||
# are usually outside of the accessible range. | ||
relative_path = path.relative_to(Path.cwd()) | ||
|
||
video_settings = { | ||
k: v | ||
for k, v in settings.items() | ||
if k in self.video_settings | ||
} | ||
|
||
return video(relative_path, **video_settings) | ||
else: | ||
warn('Could not find path in the manim output') | ||
|
||
# If we were silent, some errors could have been silenced too. | ||
if silent: | ||
# Let's break the silence: | ||
print(stdout.getvalue()) | ||
print(stderr.getvalue(), file=sys.stderr) | ||
|
||
|
||
ip = get_ipython() | ||
ip.register_magics(ManimMagics) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
IPython | ||
manimlib |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[metadata] | ||
description-file = README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from setuptools import setup | ||
from setuptools import find_packages | ||
|
||
|
||
try: | ||
from pypandoc import convert | ||
|
||
def get_long_description(file_name): | ||
return convert(file_name, 'rst', 'md') | ||
|
||
except ImportError: | ||
|
||
def get_long_description(file_name): | ||
with open(file_name) as f: | ||
return f.read() | ||
|
||
|
||
if __name__ == '__main__': | ||
setup( | ||
name='jupyter_manim', | ||
packages=find_packages(), | ||
version='0.1', | ||
license='MIT', | ||
description='Cell magic rendering displaying videos in Jupyter/IPython', | ||
long_description=get_long_description('README.md'), | ||
author='Michal Krassowski', | ||
author_email='krassowski.michal+pypi@gmail.com', | ||
url='https://github.com/krassowski/jupyter-manim', | ||
download_url='https://github.com/krassowski/jupyter-manim/tarball/v0.1', | ||
keywords=['jupyter', 'jupyterlab', 'notebook', 'manim', 'manimlib'], | ||
classifiers=[ | ||
'Development Status :: 4 - Beta', | ||
'License :: OSI Approved :: MIT License', | ||
'Framework :: IPython', | ||
'Framework :: Jupyter', | ||
'Operating System :: Microsoft :: Windows', | ||
'Operating System :: POSIX :: Linux', | ||
'Topic :: Utilities', | ||
'Topic :: Software Development :: User Interfaces', | ||
'Topic :: Software Development :: Libraries :: Python Modules', | ||
'Intended Audience :: Developers', | ||
'Intended Audience :: Science/Research', | ||
'Programming Language :: Python :: 3.6', | ||
'Programming Language :: Python :: 3.7' | ||
], | ||
install_requires=[ | ||
'manimlib', 'IPython' | ||
], | ||
) |