Skip to content

Commit 2f2ef09

Browse files
committed
Merge branch 'master' of github.com:3b1b/manim into windmill
2 parents a5641c8 + b74e5ca commit 2f2ef09

File tree

9 files changed

+262
-12
lines changed

9 files changed

+262
-12
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ python3 -m manim example_scenes.py SquareToCircle -pl
7474
### Using Docker
7575
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.
7676
77-
The prebuilt container image has manin repository included.
77+
The prebuilt container image has manim repository included.
7878
`INPUT_PATH` is where the container looks for scene files. You must set the `INPUT_PATH`
7979
environment variable to the absolute path containing your scene file and the
8080
`OUTPUT_PATH` environment variable to the directory where you want media to be written.

environment.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ channels:
55
dependencies:
66
- python=3.7
77
- cairo
8-
- ffmpeg
8+
- ffmpeg
99
- colour==0.1.5
1010
- numpy==1.15.0
1111
- pillow==5.2.0
@@ -14,4 +14,6 @@ dependencies:
1414
- opencv==3.4.2
1515
- pycairo==1.18.0
1616
- pydub==0.23.0
17-
- pyreadline
17+
- ffmpeg
18+
- pip:
19+
- pyreadline

manimlib/constants.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
VIDEO_DIR = ""
66
VIDEO_OUTPUT_DIR = ""
77
TEX_DIR = ""
8+
TEXT_DIR = ""
89

910

1011
def initialize_directories(config):
1112
global MEDIA_DIR
1213
global VIDEO_DIR
1314
global VIDEO_OUTPUT_DIR
1415
global TEX_DIR
16+
global TEXT_DIR
1517

1618
video_path_specified = config["video_dir"] or config["video_output_dir"]
1719

@@ -37,6 +39,7 @@ def initialize_directories(config):
3739
)
3840

3941
TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex")
42+
TEXT_DIR = os.path.join(MEDIA_DIR, "texts")
4043
if not video_path_specified:
4144
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
4245
VIDEO_OUTPUT_DIR = os.path.join(MEDIA_DIR, "videos")
@@ -45,10 +48,28 @@ def initialize_directories(config):
4548
else:
4649
VIDEO_DIR = config["video_dir"]
4750

48-
for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR]:
51+
for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR, TEXT_DIR]:
4952
if folder != "" and not os.path.exists(folder):
5053
os.makedirs(folder)
5154

55+
NOT_SETTING_FONT_MSG='''
56+
Warning:
57+
You haven't set font.
58+
If you are not using English, this may cause text rendering problem.
59+
You set font like:
60+
text = Text('your text', font='your font')
61+
or:
62+
class MyText(Text):
63+
CONFIG = {
64+
'font': 'My Font'
65+
}
66+
'''
67+
START_X = 30
68+
START_Y = 20
69+
NORMAL = 'NORMAL'
70+
ITALIC = 'ITALIC'
71+
OBLIQUE = 'OBLIQUE'
72+
BOLD = 'BOLD'
5273

5374
TEX_USE_CTEX = False
5475
TEX_TEXT_TO_REPLACE = "YourTextHere"

manimlib/extract_scene.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def get_scenes_to_render(scene_classes, config):
116116
)
117117
if result:
118118
return result
119-
return prompt_user_for_choice(scene_classes)
119+
return [scene_classes[0]] if len(scene_classes) == 1 else prompt_user_for_choice(scene_classes)
120120

121121

122122
def get_scene_classes_from_module(module):

manimlib/imports.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from manimlib.mobject.svg.drawings import *
5050
from manimlib.mobject.svg.svg_mobject import *
5151
from manimlib.mobject.svg.tex_mobject import *
52+
from manimlib.mobject.svg.text_mobject import *
5253
from manimlib.mobject.three_d_utils import *
5354
from manimlib.mobject.three_dimensions import *
5455
from manimlib.mobject.types.image_mobject import *

manimlib/mobject/svg/svg_mobject.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def get_mobjects_from(self, element):
8181
self.update_ref_to_element(element)
8282
elif element.tagName == 'style':
8383
pass # TODO, handle style
84-
elif element.tagName in ['g', 'svg']:
84+
elif element.tagName in ['g', 'svg', 'symbol']:
8585
result += it.chain(*[
8686
self.get_mobjects_from(child)
8787
for child in element.childNodes
@@ -284,12 +284,27 @@ def handle_transforms(self, element, mobject):
284284
pass
285285
# TODO, ...
286286

287+
def flatten(self, input_list):
288+
output_list = []
289+
for i in input_list:
290+
if isinstance(i, list):
291+
output_list.extend(self.flatten(i))
292+
else:
293+
output_list.append(i)
294+
return output_list
295+
296+
def get_all_childNodes_have_id(self, element):
297+
all_childNodes_have_id = []
298+
if not isinstance(element, minidom.Element):
299+
return
300+
if element.hasAttribute('id'):
301+
return element
302+
for e in element.childNodes:
303+
all_childNodes_have_id.append(self.get_all_childNodes_have_id(e))
304+
return self.flatten([e for e in all_childNodes_have_id if e])
305+
287306
def update_ref_to_element(self, defs):
288-
new_refs = dict([
289-
(element.getAttribute('id'), element)
290-
for element in defs.childNodes
291-
if isinstance(element, minidom.Element) and element.hasAttribute('id')
292-
])
307+
new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)])
293308
self.ref_to_element.update(new_refs)
294309

295310
def move_into_position(self):
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import re
2+
import os
3+
import copy
4+
import hashlib
5+
import cairo
6+
import manimlib.constants as consts
7+
from manimlib.constants import *
8+
from manimlib.mobject.svg.svg_mobject import SVGMobject
9+
from manimlib.utils.config_ops import digest_config
10+
11+
12+
class TextSetting(object):
13+
def __init__(self, start, end, font, slant, weight, line_num=-1):
14+
self.start = start
15+
self.end = end
16+
self.font = font
17+
self.slant = slant
18+
self.weight = weight
19+
self.line_num = line_num
20+
21+
22+
class Text(SVGMobject):
23+
CONFIG = {
24+
# Mobject
25+
'color': consts.WHITE,
26+
'height': None,
27+
# Text
28+
'font': '',
29+
'gradient': None,
30+
'lsh': -1,
31+
'size': 1,
32+
'slant': NORMAL,
33+
'weight': NORMAL,
34+
't2c': {},
35+
't2f': {},
36+
't2g': {},
37+
't2s': {},
38+
't2w': {},
39+
}
40+
41+
def __init__(self, text, **config):
42+
self.text = text
43+
self.full2short(config)
44+
digest_config(self, config)
45+
self.lsh = self.size if self.lsh == -1 else self.lsh
46+
47+
file_name = self.text2svg()
48+
SVGMobject.__init__(self, file_name, **config)
49+
50+
if self.t2c:
51+
self.set_color_by_t2c()
52+
if self.gradient:
53+
self.set_color_by_gradient(*self.gradient)
54+
if self.t2g:
55+
self.set_color_by_t2g()
56+
57+
# anti-aliasing
58+
self.scale(0.1)
59+
60+
def find_indexes(self, word):
61+
m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word)
62+
if m:
63+
start = int(m.group(1)) if m.group(1) != '' else 0
64+
end = int(m.group(2)) if m.group(2) != '' else len(self.text)
65+
start = len(self.text) + start if start < 0 else start
66+
end = len(self.text) + end if end < 0 else end
67+
return [(start, end)]
68+
69+
indexes = []
70+
index = self.text.find(word)
71+
while index != -1:
72+
indexes.append((index, index + len(word)))
73+
index = self.text.find(word, index + len(word))
74+
return indexes
75+
76+
def full2short(self, config):
77+
for kwargs in [config, self.CONFIG]:
78+
if kwargs.__contains__('line_spacing_height'):
79+
kwargs['lsh'] = kwargs.pop('line_spacing_height')
80+
if kwargs.__contains__('text2color'):
81+
kwargs['t2c'] = kwargs.pop('text2color')
82+
if kwargs.__contains__('text2font'):
83+
kwargs['t2f'] = kwargs.pop('text2font')
84+
if kwargs.__contains__('text2gradient'):
85+
kwargs['t2g'] = kwargs.pop('text2gradient')
86+
if kwargs.__contains__('text2slant'):
87+
kwargs['t2s'] = kwargs.pop('text2slant')
88+
if kwargs.__contains__('text2weight'):
89+
kwargs['t2w'] = kwargs.pop('text2weight')
90+
91+
def set_color_by_t2c(self, t2c=None):
92+
t2c = t2c if t2c else self.t2c
93+
for word, color in list(t2c.items()):
94+
for start, end in self.find_indexes(word):
95+
self[start:end].set_color(color)
96+
97+
def set_color_by_t2g(self, t2g=None):
98+
t2g = t2g if t2g else self.t2g
99+
for word, gradient in list(t2g.items()):
100+
for start, end in self.find_indexes(word):
101+
self[start:end].set_color_by_gradient(*gradient)
102+
103+
def str2slant(self, string):
104+
if string == NORMAL:
105+
return cairo.FontSlant.NORMAL
106+
if string == ITALIC:
107+
return cairo.FontSlant.ITALIC
108+
if string == OBLIQUE:
109+
return cairo.FontSlant.OBLIQUE
110+
111+
def str2weight(self, string):
112+
if string == NORMAL:
113+
return cairo.FontWeight.NORMAL
114+
if string == BOLD:
115+
return cairo.FontWeight.BOLD
116+
117+
def text2hash(self):
118+
settings = self.font + self.slant + self.weight
119+
settings += str(self.t2f) + str(self.t2s) + str(self.t2w)
120+
settings += str(self.lsh) + str(self.size)
121+
id_str = self.text+settings
122+
hasher = hashlib.sha256()
123+
hasher.update(id_str.encode())
124+
return hasher.hexdigest()[:16]
125+
126+
def text2settings(self):
127+
settings = []
128+
t2x = [self.t2f, self.t2s, self.t2w]
129+
for i in range(len(t2x)):
130+
fsw = [self.font, self.slant, self.weight]
131+
if t2x[i]:
132+
for word, x in list(t2x[i].items()):
133+
for start, end in self.find_indexes(word):
134+
fsw[i] = x
135+
settings.append(TextSetting(start, end, *fsw))
136+
137+
# Set All text settings(default font slant weight)
138+
fsw = [self.font, self.slant, self.weight]
139+
settings.sort(key=lambda setting: setting.start)
140+
temp_settings = settings.copy()
141+
start = 0
142+
for setting in settings:
143+
if setting.start != start:
144+
temp_settings.append(TextSetting(start, setting.start, *fsw))
145+
start = setting.end
146+
if start != len(self.text):
147+
temp_settings.append(TextSetting(start, len(self.text), *fsw))
148+
settings = sorted(temp_settings, key=lambda setting: setting.start)
149+
150+
if re.search(r'\n', self.text):
151+
line_num = 0
152+
for start, end in self.find_indexes('\n'):
153+
for setting in settings:
154+
if setting.line_num == -1:
155+
setting.line_num = line_num
156+
if start < setting.end:
157+
line_num += 1
158+
new_setting = copy.copy(setting)
159+
setting.end = end
160+
new_setting.start = end
161+
new_setting.line_num = line_num
162+
settings.append(new_setting)
163+
settings.sort(key=lambda setting: setting.start)
164+
break
165+
166+
for setting in settings:
167+
if setting.line_num == -1:
168+
setting.line_num = 0
169+
170+
return settings
171+
172+
def text2svg(self):
173+
# anti-aliasing
174+
size = self.size * 10
175+
lsh = self.lsh * 10
176+
177+
if self.font == '':
178+
print(NOT_SETTING_FONT_MSG)
179+
180+
dir_name = consts.TEXT_DIR
181+
hash_name = self.text2hash()
182+
file_name = os.path.join(dir_name, hash_name)+'.svg'
183+
if os.path.exists(file_name):
184+
return file_name
185+
186+
surface = cairo.SVGSurface(file_name, 600, 400)
187+
context = cairo.Context(surface)
188+
context.set_font_size(size)
189+
context.move_to(START_X, START_Y)
190+
191+
settings = self.text2settings()
192+
offset_x = 0
193+
last_line_num = 0
194+
for setting in settings:
195+
font = setting.font
196+
slant = self.str2slant(setting.slant)
197+
weight = self.str2weight(setting.weight)
198+
text = self.text[setting.start:setting.end].replace('\n', ' ')
199+
200+
context.select_font_face(font, slant, weight)
201+
if setting.line_num != last_line_num:
202+
offset_x = 0
203+
last_line_num = setting.line_num
204+
context.move_to(START_X + offset_x, START_Y + lsh*setting.line_num)
205+
context.show_text(text)
206+
offset_x += context.text_extents(text)[4]
207+
208+
return file_name

manimlib/scene/scene.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import inspect
22
import random
33
import warnings
4+
import platform
45

56
from tqdm import tqdm as ProgressDisplay
67
import numpy as np
@@ -304,6 +305,7 @@ def get_time_progression(self, run_time, n_iterations=None, override_skip_animat
304305
time_progression = ProgressDisplay(
305306
times, total=n_iterations,
306307
leave=self.leave_progress_bars,
308+
ascii=False if platform.system() != 'Windows' else True
307309
)
308310
return time_progression
309311

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ scipy==1.3.0
77
tqdm==4.24.0
88
opencv-python==3.4.2.17
99
pycairo==1.17.1; sys_platform == 'linux'
10-
pycairo>=1.18.0; sys_platform == 'win32'
10+
pycairo>=1.18.1; sys_platform == 'win32'
1111
pydub==0.23.0
12+
pyreadline==2.1; sys_platform == 'win32'

0 commit comments

Comments
 (0)