Merge branch 'master' of github.com:3b1b/manim into shadows

This commit is contained in:
Grant Sanderson 2018-12-26 11:42:05 -08:00
commit 9db4faf179
15 changed files with 475 additions and 351 deletions

14
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,14 @@
### If this is a support request:
**Please attempt to solve the problem on your own before opening an issue.**
Between old issues, StackOverflow, and Google, you should be able to find
solutions to most of the common problems.
Include at least:
1. Steps to reproduce the issue (e.g. the command you ran)
2. The unexpected behavior that occurred (e.g. error messages or screenshots)
3. The environment (e.g. operating system and version of manim)
### If this is a feature request:
Include the motivation for making this change.

9
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,9 @@
Thanks for contributing to manim!
**Please ensure that your pull request works with the latest version of manim.**
You should also include:
1. The motivation for making this change (or link the relevant issues)
2. How you tested the new behavior (e.g. a minimal working example, before/after
screenshots, gifs, commands, etc.) This is rather informal at the moment, but
the goal is to show us how you know the pull request works as intended.

131
README.md
View file

@ -1,42 +1,55 @@
# Manim
Animation engine for explanatory math videos.
# Manim - Mathematical Animation Engine
[![Documentation Status](https://readthedocs.org/projects/manim/badge/?version=latest)](https://manim.readthedocs.io/en/latest/?badge=latest)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://choosealicense.com/licenses/mit/)
[![build](https://img.shields.io/travis/3b1b/manim.svg "Travis build status")](https://travis-ci.com/3b1b/manim)
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically.
## Installation
Manim runs on python 3.7. You can install the python requirements with
`pip install -r requirements.txt`. System requirements are
[cairo](https://www.cairographics.org), [latex](https://www.latex-project.org),
[ffmpeg](https://www.ffmpeg.org), and [sox](http://sox.sourceforge.net).
For those who want to play around with this tool, I should be upfront that I've mostly had my own use cases (i.e. 3b1b videos) in mind while building it, and it might not be the most friendly thing to get up and running. In particular, I have not done a great job tracking requirements, writing
tests, and documentation, to put it euphemistically, almost exclusively takes the form of naming conventions.
For 9/10 of math animation needs, you'd probably be better off using a more well-maintained tool, like matplotlib, mathematica, Processing or even going a non-programatic route with something like After Effects or even Keynote. I also happen to think the program "Grapher" built into osx is really great, and surprisingly versatile for many needs. My own reasons for building this tool and using it for videos are twofold, and I'm not sure how well they apply to other people's use cases.
1) If I wish to work with some new type of mathematical thing (e.g. a fractal), or to experiment with a different type of animation, it's easier to work it into the underlying system and manipulate it the same way as more standard objects/animation. Admittedly, though, part of the reason I find this easier is because I'm more familiar with the underlying system here than I am with others. This keeps me from shying away from certain video topics that I would otherwise have no idea how to animate.
2) Having my own tool has been a forcing function for having a style which is more original than what I may have otherwise produced. The cost of this was slower video production when the tool was in its early days, and during points when I do some kind of rehaul, but I think the artistic benefit is a real one. If you wish to use this tool and adopt the same style, by all means feel free. In fact, I encourage it. But the tricky part about anything which confers the benefit of originality is that this benefit cannot be easily shared.
## Install requirements
Manim works with Python 3.7, and many of the older projects from the Python 2.7 days of Manim will not be supported.
Manim dependencies rely on system libraries you will need to install on your
operating system:
* ffmpeg
* latex
* sox
Then you can install the python dependencies:
### Directly
```sh
python3 -m pip install -r requirements.txt
git clone https://github.com/3b1b/manim.git
cd manim
pip install -r requirements.txt
python3 -m manim example_scenes.py SquareToCircle -pl
```
## How to Use
Todd Zimmerman put together a [very nice tutorial](https://talkingphysics.wordpress.com/2018/06/11/learning-how-to-animate-videos-using-manim-series-a-journey/) on getting started with manim. I can't make promises that future versions will always be compatible with what is discussed in that tutorial, but he certainly does a much better job than I have laying out the basics.
### Using `virtualenv` and `virtualenvwrapper`
After installing `virtualenv` and `virtualenvwrapper`
```sh
git clone https://github.com/3b1b/manim.git
mkvirtualenv -a manim -r requirements.txt manim
python3 -m manim example_scenes.py SquareToCircle -pl
```
### Using Docker
Since it's a bit tricky to get all the dependencies set up just right, there is a Dockerfile provided in this repo as well as [a premade image on Docker Hub](https://hub.docker.com/r/eulertour/manim/tags/).
The image does not contain a copy of the repo. This is intentional, as it allows you to either bind mount a repo that you've cloned locally or clone any fork/branch you want. Since test coverage is painfully lacking, the image may not have dependencies for all of manim.
1. [Install Docker](https://www.docker.com/products/overview)
2. Get the docker image
* Pull it (recommended): `docker pull eulertour/manim:latest`, or
* Build it: `docker build -t manim .`
3. Start the image
* Bind mount a local repo (recommended): `docker run -itv /absolute/path/to/your/local/manim/repo:/root/manim eulertour/manim` or
* Clone a remote repo: `docker run -it eulertour/manim`, then `git clone https://github.com/eulertour/manim.git`
4. Render an animation
```sh
cd manim
python3 -m manim example_scenes.py SquareToCircle -l
```
Note that the image doesn't have any development tools installed and can't preview animations. Its purpose is building and testing only.
## Using manim
Try running the following:
```sh
python3 extract_scene.py example_scenes.py SquareToCircle -pl
python3 -m manim example_scenes.py SquareToCircle -pl
```
The -p is for previewing, meaning the the video file will automatically open when it is done rendering.
Use -l for a faster rendering at a lower quality.
Use -s to skip to the end and just show the final frame.
@ -49,38 +62,38 @@ Look through the old_projects folder to see the code for previous 3b1b videos.
While developing a scene, the `-s` flag is helpful to just see what things look like at the end without having to generate the full animation. It can also be helpful to use the `-n` flag to skip over some number of animations.
Scenes with `PiCreatures` are somewhat 3b1b specific, so the specific designs for various expressions are not part of the public repository. You should still be able to run them, but they will fall back on using the "plain" expression for the creature.
### Documentation
Documentation is in progress at [manim.readthedocs.io](https://manim.readthedocs.io).
### (Outdated) Walkthrough
Todd Zimmerman put together a [tutorial](https://talkingphysics.wordpress.com/2018/06/11/learning-how-to-animate-videos-using-manim-series-a-journey/) on getting started with manim, but it uses an outdated version that runs on python 2.7. It may not be fully compatible with the current version of manim, but it does a good job laying out the basics.
### Live Streaming
To live stream your animations, simply run manim with the `--livestream` option.
```sh
> python -m manim --livestream
Writing to media/videos/scene/scene/1080p30/LiveStreamTemp.mp4
Manim is now running in streaming mode. Stream animations by passing
them to manim.play(), e.g.
>>> c = Circle()
>>> manim.play(ShowCreation(c))
>>>
```
It is also possible to stream directly to Twitch. To do that simply pass
--livestream and --to-twitch to manim and specify the stream key with
--with-key. Then when you follow the above example the stream will directly
start on your Twitch channel (with no audio support).
## Contributing
Is always welcome. In particular, there is a dire need for tests and documentation.
## License
All files in the directories active_projects and old_projects, which by and large generate the visuals for 3b1b videos, are copyright 3Blue1Brown.
The general purpose animation code found in the remainder of the repository, on the other hand, is under the MIT license.
## Docker Method
Since it's a bit tricky to get all the dependencies set up just right, there is
a Dockerfile provided.
1. [Install Docker](https://www.docker.com/products/overview)
2. Build docker image. `docker build -t manim .`
3. Run it! `docker run --rm -itv "$PWD":/root/manim/ manim`
On a Windows system, make sure to replace `$PWD` with an absolute path to manim.
Note that the shipped Docker image contains the bare requirements to run manim. To transform the Docker container into a fully-functioning development environment, you will have to edit your personal Dockerfile a bit. For a guide to create your own personal Docker image, consult [this guide to Dockerfiles](https://www.howtoforge.com/tutorial/how-to-create-docker-images-with-dockerfile/).
## Live Streaming
To live stream your animations, simply assign `IS_LIVE_STREAMING = True` in `constants.py` file and from your Python Interactive Shell (`python3`) import the stream starter with `from stream_starter import *` while under the project directory. This will provide a clean interactive shell to enter your commands. `manim` object is a `Manim()` instance so as soon as you play an animation with `manim.play()` your stream will start. A video player will pop-up and you can broadcast that video using [OBS Studio](https://obsproject.com/) which is the most practical way of streaming with this math animation library. An example:
```
>>> from stream_starter import *
YOUR STREAM IS READY!
>>> circle = Circle()
>>> manim.play(ShowCreation(circle))
Animation 0: ShowCreationCircle: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 60/60 [00:01<00:00, 37.30it/s]
<scene.scene.Scene object at 0x7f0756d5a8d0>
```
It is also possible to stream directly to Twitch. To do that simply assign `IS_STREAMING_TO_TWITCH = True` in `constants.py` file and put your Twitch Stream Key to `TWITCH_STREAM_KEY = "YOUR_STREAM_KEY"` and when you follow the above example the stream will directly start on your Twitch channel(with no audio support).

161
config.py Normal file
View file

@ -0,0 +1,161 @@
#!/usr/bin/env python
import argparse
import colour
import constants
import os
import sys
def parse_cli():
try:
parser = argparse.ArgumentParser()
module_location = parser.add_mutually_exclusive_group()
module_location.add_argument(
"file",
nargs="?",
help="path to file holding the python code for the scene",
)
parser.add_argument(
"scene_name",
nargs="?",
help="Name of the Scene class you want to see",
)
optional_args = [
("-p", "--preview"),
("-w", "--write_to_movie"),
("-s", "--show_last_frame"),
("-l", "--low_quality"),
("-m", "--medium_quality"),
("-g", "--save_pngs"),
("-f", "--show_file_in_finder"),
("-t", "--transparent"),
("-q", "--quiet"),
("-a", "--write_all")
]
for short_arg, long_arg in optional_args:
parser.add_argument(short_arg, long_arg, action="store_true")
parser.add_argument("-o", "--output_name")
parser.add_argument("-n", "--start_at_animation_number")
parser.add_argument("-r", "--resolution")
parser.add_argument("-c", "--color")
module_location.add_argument(
"--livestream",
action="store_true",
help="Run in streaming mode",
)
parser.add_argument(
"--to-twitch",
action="store_true",
help="Stream to twitch",
)
parser.add_argument(
"--with-key",
dest="twitch_key",
help="Stream key for twitch",
)
args = parser.parse_args()
if args.file is None and not args.livestream:
parser.print_help()
sys.exit(2)
if args.to_twitch and not args.livestream:
print("You must run in streaming mode in order to stream to twitch")
sys.exit(2)
if args.to_twitch and args.twitch_key is None:
print("Specify the twitch stream key with --with-key")
sys.exit(2)
return args
except argparse.ArgumentError as err:
print(str(err))
sys.exit(2)
def get_configuration(args):
if args.output_name is not None:
output_name_root, output_name_ext = os.path.splitext(
args.output_name)
expected_ext = '.png' if args.show_last_frame else '.mp4'
if output_name_ext not in ['', expected_ext]:
print("WARNING: The output will be to (doubly-dotted) %s%s" %
output_name_root % expected_ext)
output_name = args.output_name
else:
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
output_name = output_name_root
else:
output_name = args.output_name
config = {
"file": args.file,
"scene_name": args.scene_name,
"open_video_upon_completion": args.preview,
"show_file_in_finder": args.show_file_in_finder,
# By default, write to file
"write_to_movie": args.write_to_movie or not args.show_last_frame,
"show_last_frame": args.show_last_frame,
"save_pngs": args.save_pngs,
# If -t is passed in (for transparent), this will be RGBA
"saved_image_mode": "RGBA" if args.transparent else "RGB",
"movie_file_extension": ".mov" if args.transparent else ".mp4",
"quiet": args.quiet or args.write_all,
"ignore_waits": args.preview,
"write_all": args.write_all,
"output_name": output_name,
"start_at_animation_number": args.start_at_animation_number,
"end_at_animation_number": None,
}
# Camera configuration
config["camera_config"] = {}
if args.low_quality:
config["camera_config"].update(constants.LOW_QUALITY_CAMERA_CONFIG)
config["frame_duration"] = constants.LOW_QUALITY_FRAME_DURATION
elif args.medium_quality:
config["camera_config"].update(constants.MEDIUM_QUALITY_CAMERA_CONFIG)
config["frame_duration"] = constants.MEDIUM_QUALITY_FRAME_DURATION
else:
config["camera_config"].update(constants.PRODUCTION_QUALITY_CAMERA_CONFIG)
config["frame_duration"] = constants.PRODUCTION_QUALITY_FRAME_DURATION
# If the resolution was passed in via -r
if args.resolution:
if "," in args.resolution:
height_str, width_str = args.resolution.split(",")
height = int(height_str)
width = int(width_str)
else:
height = int(args.resolution)
width = int(16 * height / 9)
config["camera_config"].update({
"pixel_height": height,
"pixel_width": width,
})
if args.color:
try:
config["camera_config"]["background_color"] = colour.Color(args.color)
except AttributeError as err:
print("Please use a valid color")
print(err)
sys.exit(2)
# If rendering a transparent image/move, make sure the
# scene has a background opacity of 0
if args.transparent:
config["camera_config"]["background_opacity"] = 0
# Arguments related to skipping
stan = config["start_at_animation_number"]
if stan is not None:
if "," in stan:
start, end = stan.split(",")
config["start_at_animation_number"] = int(start)
config["end_at_animation_number"] = int(end)
else:
config["start_at_animation_number"] = int(stan)
config["skip_animations"] = any([
config["show_last_frame"] and not config["write_to_movie"],
config["start_at_animation_number"],
])
return config

View file

@ -1,20 +1,11 @@
import os
import numpy as np
env_MEDIA_DIR = None
MEDIA_DIR = "#ERROR#"
try:
env_MEDIA_DIR = os.getenv("MEDIA_DIR")
except NameError:
try:
env_MEDIA_DIR = os.environ['MEDIA_DIR']
except KeyError:
pass
if not (env_MEDIA_DIR is None):
# Initialize directories
env_MEDIA_DIR = os.getenv("MEDIA_DIR")
if env_MEDIA_DIR:
MEDIA_DIR = env_MEDIA_DIR
elif os.path.exists("media_dir.txt"):
elif os.path.isfile("media_dir.txt"):
with open("media_dir.txt", 'rU') as media_file:
MEDIA_DIR = media_file.readline().strip()
else:
@ -22,18 +13,77 @@ else:
os.path.expanduser('~'),
"Dropbox (3Blue1Brown)/3Blue1Brown Team Folder"
)
if not os.path.exists(MEDIA_DIR):
raise Exception("""
Redefine MEDIA_DIR by changing the MEDIA_DIR
environment constant or by changing
media_dir.txt to point to a valid directory
where movies and images will be written
""")
if not os.path.isdir(MEDIA_DIR):
MEDIA_DIR = "media"
print(
f"Media will be stored in {MEDIA_DIR + os.sep}. You can change "
"this behavior by writing a different directory to media_dir.txt."
)
with open("media_dir.txt", 'w') as media_file:
media_file.write(MEDIA_DIR)
#
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
# TODO, staged scenes should really go into a subdirectory of a given scenes directory
STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes")
###
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
FILE_DIR = os.path.join(THIS_DIR, "files")
TEX_DIR = os.path.join(FILE_DIR, "Tex")
TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing?
# These two may be depricated now.
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR,
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR,
STAGED_SCENES_DIR]:
if not os.path.exists(folder):
os.makedirs(folder)
TEX_USE_CTEX = False
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(
THIS_DIR, "tex_template.tex" if not TEX_USE_CTEX
else "ctex_template.tex"
)
with open(TEMPLATE_TEX_FILE, "r") as infile:
TEMPLATE_TEXT_FILE_BODY = infile.read()
TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace(
TEX_TEXT_TO_REPLACE,
"\\begin{align*}\n" + TEX_TEXT_TO_REPLACE + "\n\\end{align*}",
)
HELP_MESSAGE = """
Usage:
python extract_scene.py <module> [<scene name>]
-p preview in low quality
-s show and save picture of last frame
-w write result to file [this is default if nothing else is stated]
-o <file_name> write to a different file_name
-l use low quality
-m use medium quality
-a run and save every scene in the script, or all args for the given scene
-q don't print progress
-f when writing to a movie file, export the frames in png sequence
-t use transperency when exporting images
-n specify the number of the animation to start from
-r specify a resolution
-c specify a background color
"""
SCENE_NOT_FOUND_MESSAGE = """
That scene is not in the script
"""
CHOOSE_NUMBER_MESSAGE = """
Choose number corresponding to desired scene/arguments.
(Use comma separated list for multiple entries)
Choice(s): """
INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit"
NO_SCENE_MESSAGE = """
There are no scenes inside that module
"""
LOW_QUALITY_FRAME_DURATION = 1. / 15
MEDIUM_QUALITY_FRAME_DURATION = 1. / 30
@ -114,44 +164,9 @@ PI = np.pi
TAU = 2 * PI
DEGREES = TAU / 360
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
# TODO, staged scenes should really go into a subdirectory of a given scenes directory
STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes")
###
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
FILE_DIR = os.path.join(THIS_DIR, "files")
TEX_DIR = os.path.join(FILE_DIR, "Tex")
TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing?
# These two may be depricated now.
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR,
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR,
STAGED_SCENES_DIR]:
if not os.path.exists(folder):
os.makedirs(folder)
TEX_USE_CTEX = False
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(
THIS_DIR, "tex_template.tex" if not TEX_USE_CTEX
else "ctex_template.tex"
)
with open(TEMPLATE_TEX_FILE, "r") as infile:
TEMPLATE_TEXT_FILE_BODY = infile.read()
TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace(
TEX_TEXT_TO_REPLACE,
"\\begin{align*}" + TEX_TEXT_TO_REPLACE + "\\end{align*}",
)
FFMPEG_BIN = "ffmpeg"
# Colors
COLOR_MAP = {
"DARK_BLUE": "#236B8E",
"DARK_BROWN": "#8B4513",
@ -214,12 +229,17 @@ locals().update(COLOR_MAP)
for name in [s for s in list(COLOR_MAP.keys()) if s.endswith("_C")]:
locals()[name.replace("_C", "")] = locals()[name]
# Streaming related configurations
IS_LIVE_STREAMING = False
# Streaming related configuration
LIVE_STREAM_NAME = "LiveStream"
IS_STREAMING_TO_TWITCH = False
TWITCH_STREAM_KEY = "YOUR_STREAM_KEY"
STREAMING_PROTOCOL = "tcp"
STREAMING_IP = "127.0.0.1"
STREAMING_PORT = "2000"
STREAMING_CLIENT = "ffplay"
STREAMING_URL = f"{STREAMING_PROTOCOL}://{STREAMING_IP}:{STREAMING_PORT}?listen"
STREAMING_CONSOLE_BANNER = """
Manim is now running in streaming mode. Stream animations by passing
them to manim.play(), e.g.
>>> c = Circle()
>>> manim.play(ShowCreation(c))
"""

View file

@ -1,173 +1,18 @@
# !/usr/bin/env python2
# !/usr/bin/env python
import sys
import argparse
# import imp
import constants
import importlib
import inspect
import itertools as it
import os
import subprocess as sp
import sys
import traceback
from constants import *
from scene.scene import Scene
from utils.sounds import play_error_sound
from utils.sounds import play_finish_sound
from colour import Color
HELP_MESSAGE = """
Usage:
python extract_scene.py <module> [<scene name>]
-p preview in low quality
-s show and save picture of last frame
-w write result to file [this is default if nothing else is stated]
-o <file_name> write to a different file_name
-l use low quality
-m use medium quality
-a run and save every scene in the script, or all args for the given scene
-q don't print progress
-f when writing to a movie file, export the frames in png sequence
-t use transperency when exporting images
-n specify the number of the animation to start from
-r specify a resolution
-c specify a background color
"""
SCENE_NOT_FOUND_MESSAGE = """
That scene is not in the script
"""
CHOOSE_NUMBER_MESSAGE = """
Choose number corresponding to desired scene/arguments.
(Use comma separated list for multiple entries)
Choice(s): """
INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit"
NO_SCENE_MESSAGE = """
There are no scenes inside that module
"""
def get_configuration():
try:
parser = argparse.ArgumentParser()
parser.add_argument(
"file", help="path to file holding the python code for the scene"
)
parser.add_argument(
"scene_name", help="Name of the Scene class you want to see"
)
optional_args = [
("-p", "--preview"),
("-w", "--write_to_movie"),
("-s", "--show_last_frame"),
("-l", "--low_quality"),
("-m", "--medium_quality"),
("-g", "--save_pngs"),
("-f", "--show_file_in_finder"),
("-t", "--transparent"),
("-q", "--quiet"),
("-a", "--write_all")
]
for short_arg, long_arg in optional_args:
parser.add_argument(short_arg, long_arg, action="store_true")
parser.add_argument("-o", "--output_name")
parser.add_argument("-n", "--start_at_animation_number")
parser.add_argument("-r", "--resolution")
parser.add_argument("-c", "--color")
args = parser.parse_args()
if args.output_name is not None:
output_name_root, output_name_ext = os.path.splitext(
args.output_name)
expected_ext = '.png' if args.show_last_frame else '.mp4'
if output_name_ext not in ['', expected_ext]:
print("WARNING: The output will be to (doubly-dotted) %s%s" %
output_name_root % expected_ext)
output_name = args.output_name
else:
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
output_name = output_name_root
else:
output_name = args.output_name
except argparse.ArgumentError as err:
print(str(err))
sys.exit(2)
config = {
"file": args.file,
"scene_name": args.scene_name,
"open_video_upon_completion": args.preview,
"show_file_in_finder": args.show_file_in_finder,
# By default, write to file
"write_to_movie": args.write_to_movie or not args.show_last_frame,
"show_last_frame": args.show_last_frame,
"save_pngs": args.save_pngs,
# If -t is passed in (for transparent), this will be RGBA
"saved_image_mode": "RGBA" if args.transparent else "RGB",
"movie_file_extension": ".mov" if args.transparent else ".mp4",
"quiet": args.quiet or args.write_all,
"ignore_waits": args.preview,
"write_all": args.write_all,
"output_name": output_name,
"start_at_animation_number": args.start_at_animation_number,
"end_at_animation_number": None,
}
# Camera configuration
config["camera_config"] = {}
if args.low_quality:
config["camera_config"].update(LOW_QUALITY_CAMERA_CONFIG)
config["frame_duration"] = LOW_QUALITY_FRAME_DURATION
elif args.medium_quality:
config["camera_config"].update(MEDIUM_QUALITY_CAMERA_CONFIG)
config["frame_duration"] = MEDIUM_QUALITY_FRAME_DURATION
else:
config["camera_config"].update(PRODUCTION_QUALITY_CAMERA_CONFIG)
config["frame_duration"] = PRODUCTION_QUALITY_FRAME_DURATION
# If the resolution was passed in via -r
if args.resolution:
if "," in args.resolution:
height_str, width_str = args.resolution.split(",")
height = int(height_str)
width = int(width_str)
else:
height = int(args.resolution)
width = int(16 * height / 9)
config["camera_config"].update({
"pixel_height": height,
"pixel_width": width,
})
if args.color:
try:
config["camera_config"]["background_color"] = Color(args.color)
except AttributeError as err:
print("Please use a valid color")
print(err)
sys.exit(2)
# If rendering a transparent image/move, make sure the
# scene has a background opacity of 0
if args.transparent:
config["camera_config"]["background_opacity"] = 0
# Arguments related to skipping
stan = config["start_at_animation_number"]
if stan is not None:
if "," in stan:
start, end = stan.split(",")
config["start_at_animation_number"] = int(start)
config["end_at_animation_number"] = int(end)
else:
config["start_at_animation_number"] = int(stan)
config["skip_animations"] = any([
config["show_last_frame"] and not config["write_to_movie"],
config["start_at_animation_number"],
])
return config
def handle_scene(scene, **config):
import platform
@ -223,26 +68,31 @@ def prompt_user_for_choice(name_to_obj):
print("%d: %s" % (count, name))
num_to_name[count] = name
try:
user_input = input(CHOOSE_NUMBER_MESSAGE)
user_input = input(constants.CHOOSE_NUMBER_MESSAGE)
return [
name_to_obj[num_to_name[int(num_str)]]
for num_str in user_input.split(",")
]
except:
print(INVALID_NUMBER_MESSAGE)
except KeyError:
print(constants.INVALID_NUMBER_MESSAGE)
sys.exit()
user_input = input(constants.CHOOSE_NUMBER_MESSAGE)
return [
name_to_obj[num_to_name[int(num_str)]]
for num_str in user_input.split(",")
]
def get_scene_classes(scene_names_to_classes, config):
if len(scene_names_to_classes) == 0:
print(NO_SCENE_MESSAGE)
print(constants.NO_SCENE_MESSAGE)
return []
if len(scene_names_to_classes) == 1:
return list(scene_names_to_classes.values())
if config["scene_name"] in scene_names_to_classes:
return [scene_names_to_classes[config["scene_name"]]]
if config["scene_name"] != "":
print(SCENE_NOT_FOUND_MESSAGE)
print(constants.SCENE_NOT_FOUND_MESSAGE)
return []
if config["write_all"]:
return list(scene_names_to_classes.values())
@ -254,16 +104,10 @@ def get_module(file_name):
return importlib.import_module(module_name)
def main():
config = get_configuration()
def main(config):
module = get_module(config["file"])
scene_names_to_classes = dict(inspect.getmembers(module, is_scene))
# config["output_directory"] = os.path.join(
# VIDEO_DIR,
# config["file"].replace(".py", "")
# )
scene_kwargs = dict([
(key, config[key])
for key in [
@ -288,7 +132,7 @@ def main():
try:
handle_scene(SceneClass(**scene_kwargs), **config)
play_finish_sound()
except:
except Exception:
print("\n\n")
traceback.print_exc()
print("\n\n")

View file

@ -1,5 +1,6 @@
import numpy as np
import warnings
import os
from constants import *
@ -17,7 +18,12 @@ from utils.rate_functions import squish_rate_func
from utils.rate_functions import there_and_back
from utils.space_ops import get_norm
PI_CREATURE_DIR = os.path.join(MEDIA_DIR, "designs", "PiCreature")
pi_creature_dir_maybe = os.path.join(MEDIA_DIR, "designs", "PiCreature")
if os.path.exists(pi_creature_dir_maybe):
PI_CREATURE_DIR = pi_creature_dir_maybe
else:
PI_CREATURE_DIR = os.path.join(FILE_DIR)
PI_CREATURE_SCALE_FACTOR = 0.5
LEFT_EYE_INDEX = 0
@ -63,7 +69,7 @@ class PiCreature(SVGMobject):
FILE_DIR,
"PiCreatures_plain.svg",
)
SVGMobject.__init__(self, file_name=svg_file, **kwargs)
SVGMobject.__init__(self, mode="plain", file_name=svg_file, **kwargs)
if self.flip_at_start:
self.flip()

View file

@ -1,31 +1,14 @@
#!/usr/bin/python3
#!/usr/bin/env python
import config
import extract_scene
import stream_starter
from constants import *
from scene.scene import Scene
class Manim():
def __new__(cls):
kwargs = {
"scene_name": LIVE_STREAM_NAME,
"open_video_upon_completion": False,
"show_file_in_finder": False,
# By default, write to file
"write_to_movie": True,
"show_last_frame": False,
"save_pngs": False,
# If -t is passed in (for transparent), this will be RGBA
"saved_image_mode": "RGB",
"movie_file_extension": ".mp4",
"quiet": True,
"ignore_waits": False,
"write_all": False,
"name": LIVE_STREAM_NAME,
"start_at_animation_number": 0,
"end_at_animation_number": None,
"skip_animations": False,
"camera_config": HIGH_QUALITY_CAMERA_CONFIG,
"frame_duration": MEDIUM_QUALITY_FRAME_DURATION,
}
return Scene(**kwargs)
args = config.parse_cli()
if not args.livestream:
config = config.get_configuration(args)
extract_scene.main(config)
else:
stream_starter.start_livestream(
to_twitch=args.to_twitch,
twitch_key=args.twitch_key,
)

View file

@ -16,7 +16,7 @@ VECTOR_LABEL_SCALE_FACTOR = 0.8
def matrix_to_tex_string(matrix):
matrix = np.array(matrix).astype("string")
matrix = np.array(matrix).astype("str")
if matrix.ndim == 1:
matrix = matrix.reshape((matrix.size, 1))
n_rows, n_cols = matrix.shape

View file

@ -43,7 +43,7 @@ class SVGMobject(VMobject):
def __init__(self, file_name=None, **kwargs):
digest_config(self, kwargs)
self.file_name = self.file_name or file_name
self.file_name = file_name or self.file_name
self.ensure_valid_file()
VMobject.__init__(self, **kwargs)
self.move_into_position()
@ -264,8 +264,13 @@ class SVGMobject(VMobject):
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
scale_x, scale_y = string_to_numbers(transform)
mobject.scale(np.array([scale_x, scale_y, 1]))
scale_values = string_to_numbers(transform)
if len(scale_values) == 2:
scale_x, scale_y = scale_values
mobject.scale(np.array([scale_x, scale_y, 1]), about_point=ORIGIN)
elif len(scale_values) == 1:
scale = scale_values[0]
mobject.scale(np.array([scale, scale, 1]), about_point=ORIGIN)
except:
pass

View file

@ -1,4 +1,5 @@
from big_ol_pile_of_manim_imports import *
from once_useful_constructs import *
EXAMPLE_TRANFORM = [[0, 1], [-1, 1]]
TRANFORMED_VECTOR = [[1], [2]]
@ -192,7 +193,7 @@ class AboutLinearAlgebra(Scene):
def get_cross_product(self):
return TexMobject("""
\\vec\\textbf{v} \\times \\textbf{w} =
\\vec{\\textbf{v}} \\times \\textbf{w} =
\\text{Det}\\left(
\\begin{array}{ccc}
\\hat{\imath} & \\hat{\jmath} & \\hat{k} \\\\
@ -203,8 +204,8 @@ class AboutLinearAlgebra(Scene):
""")
def get_eigenvalue(self):
result = TextMobject("\\Text{Det}\\left(A - \\lambda I \\right) = 0")
result.submobjects[-5].set_color(YELLOW)
result = TexMobject("\\text{Det}\\left(A - \\lambda I \\right) = 0")
result.submobjects[0][-5].set_color(YELLOW)
return result
def get_matrix_multiplication_question(self):

View file

@ -0,0 +1,28 @@
import constants
import os
import importlib
modules = filter(
lambda x: x.endswith(".py"),
os.listdir(constants.THIS_DIR + os.sep + "once_useful_constructs"),
)
modules = list(map(
lambda x: x[:x.find(".py")],
modules
))
for m in modules:
if m == "__init__":
continue
else:
importlib.import_module("once_useful_constructs." + m, package="once_useful_constructs")
for m in modules:
if m == "__init__":
continue
m = globals()[m]
module_dict = m.__dict__
try:
to_import = m.__all__
except AttributeError:
to_import = [name for name in module_dict if not name.startswith('_')]
globals().update({name: module_dict[name] for name in to_import})

View file

@ -51,6 +51,9 @@ class Scene(Container):
"random_seed": 0,
"start_at_animation_number": None,
"end_at_animation_number": None,
"livestreaming": False,
"to_twitch": False,
"twitch_key": None,
}
def __init__(self, **kwargs):
@ -76,7 +79,7 @@ class Scene(Container):
self.setup()
if self.write_to_movie:
self.open_movie_pipe()
if IS_LIVE_STREAMING:
if self.livestreaming:
return None
try:
self.construct(*self.construct_args)
@ -464,7 +467,7 @@ class Scene(Container):
raise EndSceneEarlyException()
def play(self, *args, **kwargs):
if IS_LIVE_STREAMING:
if self.livestreaming:
self.stream_lock = False
if len(args) == 0:
warnings.warn("Called Scene.play with no animations")
@ -503,7 +506,7 @@ class Scene(Container):
self.continual_update(0)
self.num_plays += 1
if IS_LIVE_STREAMING:
if self.livestreaming:
self.stream_lock = True
thread.start_new_thread(self.idle_stream, ())
@ -653,10 +656,10 @@ class Scene(Container):
'-vcodec', 'libx264',
'-pix_fmt', 'yuv420p',
]
if IS_LIVE_STREAMING:
if IS_STREAMING_TO_TWITCH:
if self.livestreaming:
if self.to_twitch:
command += ['-f', 'flv']
command += ['rtmp://live.twitch.tv/app/' + TWITCH_STREAM_KEY]
command += ['rtmp://live.twitch.tv/app/' + self.twitch_key]
else:
command += ['-f', 'mpegts']
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
@ -668,7 +671,7 @@ class Scene(Container):
def close_movie_pipe(self):
self.writing_process.stdin.close()
self.writing_process.wait()
if IS_LIVE_STREAMING:
if self.livestreaming:
return True
if os.name == 'nt':
shutil.move(*self.args_to_rename_file)

View file

@ -1,13 +1,53 @@
from big_ol_pile_of_manim_imports import *
import subprocess
from scene.scene import Scene
from time import sleep
from manim import Manim
import code
import constants
import os
import readline
import subprocess
if not IS_STREAMING_TO_TWITCH:
FNULL = open(os.devnull, 'w')
subprocess.Popen([STREAMING_CLIENT, STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT + '?listen'], stdout=FNULL, stderr=FNULL)
sleep(3)
manim = Manim()
def start_livestream(to_twitch=False, twitch_key=None):
class Manim():
print("YOUR STREAM IS READY!")
def __new__(cls):
kwargs = {
"scene_name": constants.LIVE_STREAM_NAME,
"open_video_upon_completion": False,
"show_file_in_finder": False,
# By default, write to file
"write_to_movie": True,
"show_last_frame": False,
"save_pngs": False,
# If -t is passed in (for transparent), this will be RGBA
"saved_image_mode": "RGB",
"movie_file_extension": ".mp4",
"quiet": True,
"ignore_waits": False,
"write_all": False,
"name": constants.LIVE_STREAM_NAME,
"start_at_animation_number": 0,
"end_at_animation_number": None,
"skip_animations": False,
"camera_config": constants.HIGH_QUALITY_CAMERA_CONFIG,
"frame_duration": constants.MEDIUM_QUALITY_FRAME_DURATION,
"livestreaming": True,
"to_twitch": to_twitch,
"twitch_key": twitch_key,
}
return Scene(**kwargs)
if not to_twitch:
FNULL = open(os.devnull, 'w')
subprocess.Popen(
[constants.STREAMING_CLIENT, constants.STREAMING_URL],
stdout=FNULL,
stderr=FNULL)
sleep(3)
variables = globals().copy()
variables.update(locals())
shell = code.InteractiveConsole(variables)
shell.push("manim = Manim()")
shell.push("from big_ol_pile_of_manim_imports import *")
shell.interact(banner=constants.STREAMING_CONSOLE_BANNER)

View file

@ -1,6 +1,7 @@
import inspect
import os
from constants import THIS_DIR
from constants import VIDEO_DIR
@ -21,11 +22,7 @@ def guarantee_existance(path):
def get_scene_output_directory(scene_class):
file_path = os.path.abspath(inspect.getfile(scene_class))
# TODO, is there a better way to do this?
parts = file_path.split(os.path.sep)
if "manim" in parts:
sub_parts = parts[parts.index("manim") + 1:]
file_path = os.path.join(*sub_parts)
file_path = os.path.relpath(file_path, THIS_DIR)
file_path = file_path.replace(".pyc", "")
file_path = file_path.replace(".py", "")
return guarantee_existance(os.path.join(VIDEO_DIR, file_path))