mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Merge branch 'master' of github.com:3b1b/manim into windmill
This commit is contained in:
commit
2f2ef09c92
9 changed files with 262 additions and 12 deletions
|
@ -74,7 +74,7 @@ python3 -m manim example_scenes.py SquareToCircle -pl
|
||||||
### Using Docker
|
### Using Docker
|
||||||
Since it's a bit tricky to get all the dependencies set up just right, there is a Dockerfile and Compose file provided in this repo as well as [a premade image on Docker Hub](https://hub.docker.com/r/eulertour/manim/tags/). The Dockerfile contains instructions on how to build a manim image, while the Compose file contains instructions on how to run the image.
|
Since it's a bit tricky to get all the dependencies set up just right, there is a Dockerfile and Compose file provided in this repo as well as [a premade image on Docker Hub](https://hub.docker.com/r/eulertour/manim/tags/). The Dockerfile contains instructions on how to build a manim image, while the Compose file contains instructions on how to run the image.
|
||||||
|
|
||||||
The prebuilt container image has manin repository included.
|
The prebuilt container image has manim repository included.
|
||||||
`INPUT_PATH` is where the container looks for scene files. You must set the `INPUT_PATH`
|
`INPUT_PATH` is where the container looks for scene files. You must set the `INPUT_PATH`
|
||||||
environment variable to the absolute path containing your scene file and the
|
environment variable to the absolute path containing your scene file and the
|
||||||
`OUTPUT_PATH` environment variable to the directory where you want media to be written.
|
`OUTPUT_PATH` environment variable to the directory where you want media to be written.
|
||||||
|
|
|
@ -5,7 +5,7 @@ channels:
|
||||||
dependencies:
|
dependencies:
|
||||||
- python=3.7
|
- python=3.7
|
||||||
- cairo
|
- cairo
|
||||||
- ffmpeg
|
- ffmpeg
|
||||||
- colour==0.1.5
|
- colour==0.1.5
|
||||||
- numpy==1.15.0
|
- numpy==1.15.0
|
||||||
- pillow==5.2.0
|
- pillow==5.2.0
|
||||||
|
@ -14,4 +14,6 @@ dependencies:
|
||||||
- opencv==3.4.2
|
- opencv==3.4.2
|
||||||
- pycairo==1.18.0
|
- pycairo==1.18.0
|
||||||
- pydub==0.23.0
|
- pydub==0.23.0
|
||||||
- pyreadline
|
- ffmpeg
|
||||||
|
- pip:
|
||||||
|
- pyreadline
|
||||||
|
|
|
@ -5,6 +5,7 @@ MEDIA_DIR = ""
|
||||||
VIDEO_DIR = ""
|
VIDEO_DIR = ""
|
||||||
VIDEO_OUTPUT_DIR = ""
|
VIDEO_OUTPUT_DIR = ""
|
||||||
TEX_DIR = ""
|
TEX_DIR = ""
|
||||||
|
TEXT_DIR = ""
|
||||||
|
|
||||||
|
|
||||||
def initialize_directories(config):
|
def initialize_directories(config):
|
||||||
|
@ -12,6 +13,7 @@ def initialize_directories(config):
|
||||||
global VIDEO_DIR
|
global VIDEO_DIR
|
||||||
global VIDEO_OUTPUT_DIR
|
global VIDEO_OUTPUT_DIR
|
||||||
global TEX_DIR
|
global TEX_DIR
|
||||||
|
global TEXT_DIR
|
||||||
|
|
||||||
video_path_specified = config["video_dir"] or config["video_output_dir"]
|
video_path_specified = config["video_dir"] or config["video_output_dir"]
|
||||||
|
|
||||||
|
@ -37,6 +39,7 @@ def initialize_directories(config):
|
||||||
)
|
)
|
||||||
|
|
||||||
TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex")
|
TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex")
|
||||||
|
TEXT_DIR = os.path.join(MEDIA_DIR, "texts")
|
||||||
if not video_path_specified:
|
if not video_path_specified:
|
||||||
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||||
VIDEO_OUTPUT_DIR = os.path.join(MEDIA_DIR, "videos")
|
VIDEO_OUTPUT_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||||
|
@ -45,10 +48,28 @@ def initialize_directories(config):
|
||||||
else:
|
else:
|
||||||
VIDEO_DIR = config["video_dir"]
|
VIDEO_DIR = config["video_dir"]
|
||||||
|
|
||||||
for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR]:
|
for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR, TEXT_DIR]:
|
||||||
if folder != "" and not os.path.exists(folder):
|
if folder != "" and not os.path.exists(folder):
|
||||||
os.makedirs(folder)
|
os.makedirs(folder)
|
||||||
|
|
||||||
|
NOT_SETTING_FONT_MSG='''
|
||||||
|
Warning:
|
||||||
|
You haven't set font.
|
||||||
|
If you are not using English, this may cause text rendering problem.
|
||||||
|
You set font like:
|
||||||
|
text = Text('your text', font='your font')
|
||||||
|
or:
|
||||||
|
class MyText(Text):
|
||||||
|
CONFIG = {
|
||||||
|
'font': 'My Font'
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
START_X = 30
|
||||||
|
START_Y = 20
|
||||||
|
NORMAL = 'NORMAL'
|
||||||
|
ITALIC = 'ITALIC'
|
||||||
|
OBLIQUE = 'OBLIQUE'
|
||||||
|
BOLD = 'BOLD'
|
||||||
|
|
||||||
TEX_USE_CTEX = False
|
TEX_USE_CTEX = False
|
||||||
TEX_TEXT_TO_REPLACE = "YourTextHere"
|
TEX_TEXT_TO_REPLACE = "YourTextHere"
|
||||||
|
|
|
@ -116,7 +116,7 @@ def get_scenes_to_render(scene_classes, config):
|
||||||
)
|
)
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
return prompt_user_for_choice(scene_classes)
|
return [scene_classes[0]] if len(scene_classes) == 1 else prompt_user_for_choice(scene_classes)
|
||||||
|
|
||||||
|
|
||||||
def get_scene_classes_from_module(module):
|
def get_scene_classes_from_module(module):
|
||||||
|
|
|
@ -49,6 +49,7 @@ from manimlib.mobject.svg.brace import *
|
||||||
from manimlib.mobject.svg.drawings import *
|
from manimlib.mobject.svg.drawings import *
|
||||||
from manimlib.mobject.svg.svg_mobject import *
|
from manimlib.mobject.svg.svg_mobject import *
|
||||||
from manimlib.mobject.svg.tex_mobject import *
|
from manimlib.mobject.svg.tex_mobject import *
|
||||||
|
from manimlib.mobject.svg.text_mobject import *
|
||||||
from manimlib.mobject.three_d_utils import *
|
from manimlib.mobject.three_d_utils import *
|
||||||
from manimlib.mobject.three_dimensions import *
|
from manimlib.mobject.three_dimensions import *
|
||||||
from manimlib.mobject.types.image_mobject import *
|
from manimlib.mobject.types.image_mobject import *
|
||||||
|
|
|
@ -81,7 +81,7 @@ class SVGMobject(VMobject):
|
||||||
self.update_ref_to_element(element)
|
self.update_ref_to_element(element)
|
||||||
elif element.tagName == 'style':
|
elif element.tagName == 'style':
|
||||||
pass # TODO, handle style
|
pass # TODO, handle style
|
||||||
elif element.tagName in ['g', 'svg']:
|
elif element.tagName in ['g', 'svg', 'symbol']:
|
||||||
result += it.chain(*[
|
result += it.chain(*[
|
||||||
self.get_mobjects_from(child)
|
self.get_mobjects_from(child)
|
||||||
for child in element.childNodes
|
for child in element.childNodes
|
||||||
|
@ -284,12 +284,27 @@ class SVGMobject(VMobject):
|
||||||
pass
|
pass
|
||||||
# TODO, ...
|
# TODO, ...
|
||||||
|
|
||||||
|
def flatten(self, input_list):
|
||||||
|
output_list = []
|
||||||
|
for i in input_list:
|
||||||
|
if isinstance(i, list):
|
||||||
|
output_list.extend(self.flatten(i))
|
||||||
|
else:
|
||||||
|
output_list.append(i)
|
||||||
|
return output_list
|
||||||
|
|
||||||
|
def get_all_childNodes_have_id(self, element):
|
||||||
|
all_childNodes_have_id = []
|
||||||
|
if not isinstance(element, minidom.Element):
|
||||||
|
return
|
||||||
|
if element.hasAttribute('id'):
|
||||||
|
return element
|
||||||
|
for e in element.childNodes:
|
||||||
|
all_childNodes_have_id.append(self.get_all_childNodes_have_id(e))
|
||||||
|
return self.flatten([e for e in all_childNodes_have_id if e])
|
||||||
|
|
||||||
def update_ref_to_element(self, defs):
|
def update_ref_to_element(self, defs):
|
||||||
new_refs = dict([
|
new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)])
|
||||||
(element.getAttribute('id'), element)
|
|
||||||
for element in defs.childNodes
|
|
||||||
if isinstance(element, minidom.Element) and element.hasAttribute('id')
|
|
||||||
])
|
|
||||||
self.ref_to_element.update(new_refs)
|
self.ref_to_element.update(new_refs)
|
||||||
|
|
||||||
def move_into_position(self):
|
def move_into_position(self):
|
||||||
|
|
208
manimlib/mobject/svg/text_mobject.py
Normal file
208
manimlib/mobject/svg/text_mobject.py
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import cairo
|
||||||
|
import manimlib.constants as consts
|
||||||
|
from manimlib.constants import *
|
||||||
|
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||||
|
from manimlib.utils.config_ops import digest_config
|
||||||
|
|
||||||
|
|
||||||
|
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 Text(SVGMobject):
|
||||||
|
CONFIG = {
|
||||||
|
# Mobject
|
||||||
|
'color': consts.WHITE,
|
||||||
|
'height': None,
|
||||||
|
# Text
|
||||||
|
'font': '',
|
||||||
|
'gradient': None,
|
||||||
|
'lsh': -1,
|
||||||
|
'size': 1,
|
||||||
|
'slant': NORMAL,
|
||||||
|
'weight': NORMAL,
|
||||||
|
't2c': {},
|
||||||
|
't2f': {},
|
||||||
|
't2g': {},
|
||||||
|
't2s': {},
|
||||||
|
't2w': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, text, **config):
|
||||||
|
self.text = text
|
||||||
|
self.full2short(config)
|
||||||
|
digest_config(self, config)
|
||||||
|
self.lsh = self.size if self.lsh == -1 else self.lsh
|
||||||
|
|
||||||
|
file_name = self.text2svg()
|
||||||
|
SVGMobject.__init__(self, file_name, **config)
|
||||||
|
|
||||||
|
if self.t2c:
|
||||||
|
self.set_color_by_t2c()
|
||||||
|
if self.gradient:
|
||||||
|
self.set_color_by_gradient(*self.gradient)
|
||||||
|
if self.t2g:
|
||||||
|
self.set_color_by_t2g()
|
||||||
|
|
||||||
|
# anti-aliasing
|
||||||
|
self.scale(0.1)
|
||||||
|
|
||||||
|
def find_indexes(self, word):
|
||||||
|
m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word)
|
||||||
|
if m:
|
||||||
|
start = int(m.group(1)) if m.group(1) != '' else 0
|
||||||
|
end = int(m.group(2)) if m.group(2) != '' else len(self.text)
|
||||||
|
start = len(self.text) + start if start < 0 else start
|
||||||
|
end = len(self.text) + end if end < 0 else end
|
||||||
|
return [(start, end)]
|
||||||
|
|
||||||
|
indexes = []
|
||||||
|
index = self.text.find(word)
|
||||||
|
while index != -1:
|
||||||
|
indexes.append((index, index + len(word)))
|
||||||
|
index = self.text.find(word, index + len(word))
|
||||||
|
return indexes
|
||||||
|
|
||||||
|
def full2short(self, config):
|
||||||
|
for kwargs in [config, self.CONFIG]:
|
||||||
|
if kwargs.__contains__('line_spacing_height'):
|
||||||
|
kwargs['lsh'] = kwargs.pop('line_spacing_height')
|
||||||
|
if kwargs.__contains__('text2color'):
|
||||||
|
kwargs['t2c'] = kwargs.pop('text2color')
|
||||||
|
if kwargs.__contains__('text2font'):
|
||||||
|
kwargs['t2f'] = kwargs.pop('text2font')
|
||||||
|
if kwargs.__contains__('text2gradient'):
|
||||||
|
kwargs['t2g'] = kwargs.pop('text2gradient')
|
||||||
|
if kwargs.__contains__('text2slant'):
|
||||||
|
kwargs['t2s'] = kwargs.pop('text2slant')
|
||||||
|
if kwargs.__contains__('text2weight'):
|
||||||
|
kwargs['t2w'] = kwargs.pop('text2weight')
|
||||||
|
|
||||||
|
def set_color_by_t2c(self, t2c=None):
|
||||||
|
t2c = t2c if t2c else self.t2c
|
||||||
|
for word, color in list(t2c.items()):
|
||||||
|
for start, end in self.find_indexes(word):
|
||||||
|
self[start:end].set_color(color)
|
||||||
|
|
||||||
|
def set_color_by_t2g(self, t2g=None):
|
||||||
|
t2g = t2g if t2g else self.t2g
|
||||||
|
for word, gradient in list(t2g.items()):
|
||||||
|
for start, end in self.find_indexes(word):
|
||||||
|
self[start:end].set_color_by_gradient(*gradient)
|
||||||
|
|
||||||
|
def str2slant(self, string):
|
||||||
|
if string == NORMAL:
|
||||||
|
return cairo.FontSlant.NORMAL
|
||||||
|
if string == ITALIC:
|
||||||
|
return cairo.FontSlant.ITALIC
|
||||||
|
if string == OBLIQUE:
|
||||||
|
return cairo.FontSlant.OBLIQUE
|
||||||
|
|
||||||
|
def str2weight(self, string):
|
||||||
|
if string == NORMAL:
|
||||||
|
return cairo.FontWeight.NORMAL
|
||||||
|
if string == BOLD:
|
||||||
|
return cairo.FontWeight.BOLD
|
||||||
|
|
||||||
|
def text2hash(self):
|
||||||
|
settings = self.font + self.slant + self.weight
|
||||||
|
settings += str(self.t2f) + str(self.t2s) + str(self.t2w)
|
||||||
|
settings += str(self.lsh) + str(self.size)
|
||||||
|
id_str = self.text+settings
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
hasher.update(id_str.encode())
|
||||||
|
return hasher.hexdigest()[:16]
|
||||||
|
|
||||||
|
def text2settings(self):
|
||||||
|
settings = []
|
||||||
|
t2x = [self.t2f, self.t2s, self.t2w]
|
||||||
|
for i in range(len(t2x)):
|
||||||
|
fsw = [self.font, self.slant, self.weight]
|
||||||
|
if t2x[i]:
|
||||||
|
for word, x in list(t2x[i].items()):
|
||||||
|
for start, end in self.find_indexes(word):
|
||||||
|
fsw[i] = x
|
||||||
|
settings.append(TextSetting(start, end, *fsw))
|
||||||
|
|
||||||
|
# Set All text settings(default font slant weight)
|
||||||
|
fsw = [self.font, self.slant, self.weight]
|
||||||
|
settings.sort(key=lambda setting: setting.start)
|
||||||
|
temp_settings = settings.copy()
|
||||||
|
start = 0
|
||||||
|
for setting in settings:
|
||||||
|
if setting.start != start:
|
||||||
|
temp_settings.append(TextSetting(start, setting.start, *fsw))
|
||||||
|
start = setting.end
|
||||||
|
if start != len(self.text):
|
||||||
|
temp_settings.append(TextSetting(start, len(self.text), *fsw))
|
||||||
|
settings = sorted(temp_settings, key=lambda setting: setting.start)
|
||||||
|
|
||||||
|
if re.search(r'\n', self.text):
|
||||||
|
line_num = 0
|
||||||
|
for start, end in self.find_indexes('\n'):
|
||||||
|
for setting in settings:
|
||||||
|
if setting.line_num == -1:
|
||||||
|
setting.line_num = line_num
|
||||||
|
if start < setting.end:
|
||||||
|
line_num += 1
|
||||||
|
new_setting = copy.copy(setting)
|
||||||
|
setting.end = end
|
||||||
|
new_setting.start = end
|
||||||
|
new_setting.line_num = line_num
|
||||||
|
settings.append(new_setting)
|
||||||
|
settings.sort(key=lambda setting: setting.start)
|
||||||
|
break
|
||||||
|
|
||||||
|
for setting in settings:
|
||||||
|
if setting.line_num == -1:
|
||||||
|
setting.line_num = 0
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def text2svg(self):
|
||||||
|
# anti-aliasing
|
||||||
|
size = self.size * 10
|
||||||
|
lsh = self.lsh * 10
|
||||||
|
|
||||||
|
if self.font == '':
|
||||||
|
print(NOT_SETTING_FONT_MSG)
|
||||||
|
|
||||||
|
dir_name = consts.TEXT_DIR
|
||||||
|
hash_name = self.text2hash()
|
||||||
|
file_name = os.path.join(dir_name, hash_name)+'.svg'
|
||||||
|
if os.path.exists(file_name):
|
||||||
|
return file_name
|
||||||
|
|
||||||
|
surface = cairo.SVGSurface(file_name, 600, 400)
|
||||||
|
context = cairo.Context(surface)
|
||||||
|
context.set_font_size(size)
|
||||||
|
context.move_to(START_X, START_Y)
|
||||||
|
|
||||||
|
settings = self.text2settings()
|
||||||
|
offset_x = 0
|
||||||
|
last_line_num = 0
|
||||||
|
for setting in settings:
|
||||||
|
font = setting.font
|
||||||
|
slant = self.str2slant(setting.slant)
|
||||||
|
weight = self.str2weight(setting.weight)
|
||||||
|
text = self.text[setting.start:setting.end].replace('\n', ' ')
|
||||||
|
|
||||||
|
context.select_font_face(font, slant, weight)
|
||||||
|
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 + lsh*setting.line_num)
|
||||||
|
context.show_text(text)
|
||||||
|
offset_x += context.text_extents(text)[4]
|
||||||
|
|
||||||
|
return file_name
|
|
@ -1,6 +1,7 @@
|
||||||
import inspect
|
import inspect
|
||||||
import random
|
import random
|
||||||
import warnings
|
import warnings
|
||||||
|
import platform
|
||||||
|
|
||||||
from tqdm import tqdm as ProgressDisplay
|
from tqdm import tqdm as ProgressDisplay
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -304,6 +305,7 @@ class Scene(Container):
|
||||||
time_progression = ProgressDisplay(
|
time_progression = ProgressDisplay(
|
||||||
times, total=n_iterations,
|
times, total=n_iterations,
|
||||||
leave=self.leave_progress_bars,
|
leave=self.leave_progress_bars,
|
||||||
|
ascii=False if platform.system() != 'Windows' else True
|
||||||
)
|
)
|
||||||
return time_progression
|
return time_progression
|
||||||
|
|
||||||
|
|
|
@ -7,5 +7,6 @@ scipy==1.3.0
|
||||||
tqdm==4.24.0
|
tqdm==4.24.0
|
||||||
opencv-python==3.4.2.17
|
opencv-python==3.4.2.17
|
||||||
pycairo==1.17.1; sys_platform == 'linux'
|
pycairo==1.17.1; sys_platform == 'linux'
|
||||||
pycairo>=1.18.0; sys_platform == 'win32'
|
pycairo>=1.18.1; sys_platform == 'win32'
|
||||||
pydub==0.23.0
|
pydub==0.23.0
|
||||||
|
pyreadline==2.1; sys_platform == 'win32'
|
||||||
|
|
Loading…
Add table
Reference in a new issue