Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
krassowski committed May 22, 2019
1 parent 60004a5 commit ca29979
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 1 deletion.
70 changes: 69 additions & 1 deletion README.md
@@ -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`
155 changes: 155 additions & 0 deletions jupyter_manim/__init__.py
@@ -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)
2 changes: 2 additions & 0 deletions requirements.txt
@@ -0,0 +1,2 @@
IPython
manimlib
Binary file added screenshots/cell_magic_demo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions setup.cfg
@@ -0,0 +1,2 @@
[metadata]
description-file = README.md
49 changes: 49 additions & 0 deletions setup.py
@@ -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'
],
)

0 comments on commit ca29979

Please sign in to comment.