mirror of
https://github.com/3b1b/manim.git
synced 2025-09-19 04:41:56 +00:00
new Text mobject
This commit is contained in:
parent
519f82f1e7
commit
057a92f92c
4 changed files with 370 additions and 8 deletions
|
@ -5,6 +5,7 @@ MEDIA_DIR = ""
|
|||
VIDEO_DIR = ""
|
||||
VIDEO_OUTPUT_DIR = ""
|
||||
TEX_DIR = ""
|
||||
TEXT_DIR = ""
|
||||
|
||||
|
||||
def initialize_directories(config):
|
||||
|
@ -12,6 +13,7 @@ def initialize_directories(config):
|
|||
global VIDEO_DIR
|
||||
global VIDEO_OUTPUT_DIR
|
||||
global TEX_DIR
|
||||
global TEXT_DIR
|
||||
|
||||
video_path_specified = config["video_dir"] or config["video_output_dir"]
|
||||
|
||||
|
@ -36,7 +38,8 @@ def initialize_directories(config):
|
|||
"directory were both passed"
|
||||
)
|
||||
|
||||
TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex")
|
||||
TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "texs")
|
||||
TEXT_DIR = os.path.join(MEDIA_DIR, "texts")
|
||||
if not video_path_specified:
|
||||
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||
VIDEO_OUTPUT_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||
|
@ -45,10 +48,23 @@ def initialize_directories(config):
|
|||
else:
|
||||
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):
|
||||
os.makedirs(folder)
|
||||
|
||||
DEFAULT_FONT = ''
|
||||
DEFAULT_LSH = 1
|
||||
DEFAULT_SIZE = 1
|
||||
NOT_SETTING_FONT_MSG='''
|
||||
Warning:
|
||||
You haven't set DEFAULT_FONT.
|
||||
If you are not using English, this may cause text rendering problem.
|
||||
You can change the DEFAULT_FONT in manimlib\\constans.py or Text('your text', font='your font').
|
||||
'''
|
||||
NORMAL = 'NORMAL'
|
||||
ITALIC = 'ITALIC'
|
||||
OBLIQUE = 'OBLIQUE'
|
||||
BOLD = 'BOLD'
|
||||
|
||||
TEX_USE_CTEX = False
|
||||
TEX_TEXT_TO_REPLACE = "YourTextHere"
|
||||
|
|
|
@ -49,6 +49,7 @@ from manimlib.mobject.svg.brace import *
|
|||
from manimlib.mobject.svg.drawings import *
|
||||
from manimlib.mobject.svg.svg_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_dimensions import *
|
||||
from manimlib.mobject.types.image_mobject import *
|
||||
|
|
|
@ -81,7 +81,7 @@ class SVGMobject(VMobject):
|
|||
self.update_ref_to_element(element)
|
||||
elif element.tagName == 'style':
|
||||
pass # TODO, handle style
|
||||
elif element.tagName in ['g', 'svg']:
|
||||
elif element.tagName in ['g', 'svg', 'symbol']:
|
||||
result += it.chain(*[
|
||||
self.get_mobjects_from(child)
|
||||
for child in element.childNodes
|
||||
|
@ -284,12 +284,33 @@ class SVGMobject(VMobject):
|
|||
pass
|
||||
# TODO, ...
|
||||
|
||||
def flatten(self, input_list):
|
||||
output_list = []
|
||||
while True:
|
||||
if input_list == []:
|
||||
break
|
||||
for index, i in enumerate(input_list):
|
||||
if type(i)== list:
|
||||
input_list = i + input_list[index+1:]
|
||||
break
|
||||
else:
|
||||
output_list.append(i)
|
||||
input_list.pop(index)
|
||||
break
|
||||
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):
|
||||
new_refs = dict([
|
||||
(element.getAttribute('id'), element)
|
||||
for element in defs.childNodes
|
||||
if isinstance(element, minidom.Element) and element.hasAttribute('id')
|
||||
])
|
||||
new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)])
|
||||
self.ref_to_element.update(new_refs)
|
||||
|
||||
def move_into_position(self):
|
||||
|
|
324
manimlib/mobject/svg/text_mobject.py
Normal file
324
manimlib/mobject/svg/text_mobject.py
Normal file
|
@ -0,0 +1,324 @@
|
|||
import cairo
|
||||
import copy
|
||||
import hashlib
|
||||
import re
|
||||
import os
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
import manimlib.constants as consts
|
||||
|
||||
|
||||
class TextStyle(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):
|
||||
'''
|
||||
Params:
|
||||
-------
|
||||
text ::
|
||||
a str, the space(' ') in front or back and '\\n' and '\\t' will be ignored
|
||||
|
||||
Params(optional):
|
||||
-----------------
|
||||
color ::
|
||||
color defined in constants.py or a str like '#FFFFFF', default is WHITE
|
||||
|
||||
font ::
|
||||
a str, the name of font like 'Source Han Sans', default is DEFAULT_FONT, defined in constants.py
|
||||
|
||||
lsh (line_spacing_height) ::
|
||||
a number, better larger than 0.1(due to anti-aliasing), irrelevant with MUnit, default is DEFAULT_LSH
|
||||
|
||||
size ::
|
||||
a number, better larger than 0.1(due to anti-aliasing), irrelevant with MUnit, default is DEFAULT_SIZE
|
||||
|
||||
slant ::
|
||||
NORMAL or ITALIC, default is NORMAL, defined in constants.py(a str actually)
|
||||
|
||||
weight ::
|
||||
NORMAL or BOLD, default is NORMAL, defined in constants.py(a str actually)
|
||||
|
||||
fill_color ::
|
||||
the same as color
|
||||
|
||||
fill_opacity ::
|
||||
a float, default is 1
|
||||
|
||||
stroke_color ::
|
||||
the same as color
|
||||
|
||||
storke_opacity ::
|
||||
a float
|
||||
|
||||
t2c (text2color) ::
|
||||
a dict like {'text':color} or Accurate mode
|
||||
|
||||
t2f (text2font) ::
|
||||
a dict like {'text':font} or Accurate mode
|
||||
|
||||
t2s (text2slant) ::
|
||||
a dict like {'text':slant} or Accurate mode
|
||||
|
||||
t2w (text2weight) ::
|
||||
a dict like {'text':weight} or Accurate mode
|
||||
|
||||
Functions :
|
||||
-----------
|
||||
set_color(mobject function) ::
|
||||
param color, this will set the color of the whole text
|
||||
|
||||
set_text_color ::
|
||||
param t2c, the same as the t2c mentioned above(require a dict!)
|
||||
|
||||
Accurate mode:
|
||||
--------------
|
||||
This will help you to choose a specific text just like slicing, e.g. ::
|
||||
text = Text('ooo', t2c={'[:1]':RED, '[1:2]':GREEN, '[2:]':BLUE})
|
||||
|
||||
btw, you can use '[[:]]' to represent the text '[:]'
|
||||
'''
|
||||
CONFIG = {
|
||||
"color": WHITE,
|
||||
"fill_opacity": 1,
|
||||
"height": None,
|
||||
}
|
||||
|
||||
def __init__(self, text, **kwargs):
|
||||
self.text = text
|
||||
|
||||
kwargs = self.full2short(**kwargs)
|
||||
file_name = self.text2svg(text, **kwargs)
|
||||
SVGMobject.__init__(self, file_name=file_name, **kwargs)
|
||||
if kwargs.__contains__('t2c'):
|
||||
self.text2color(text, **kwargs)
|
||||
#anti-aliasing
|
||||
self.scale(0.1)
|
||||
|
||||
def full2short(self, **kwargs):
|
||||
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__('text2slant'):
|
||||
kwargs['t2s'] = kwargs.pop('text2slant')
|
||||
if kwargs.__contains__('text2weight'):
|
||||
kwargs['t2w'] = kwargs.pop('text2weight')
|
||||
return kwargs
|
||||
|
||||
def find_indexes(self, text, word):
|
||||
indexes = []
|
||||
if re.match(r'\[\[[0-9\-]{0,}:[0-9\-]{0,}\]\]', word):
|
||||
word = word[1:-1]
|
||||
index = text.find(word)
|
||||
while index != -1:
|
||||
indexes.append((index, index+len(word)))
|
||||
index = text.find(word, index+len(word))
|
||||
return indexes
|
||||
|
||||
def find_strat_and_end(self, text, word):
|
||||
m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word)
|
||||
start = int(m.group(1)) if m.group(1) != '' else 0
|
||||
end = int(m.group(2)) if m.group(2) != '' else len(text)
|
||||
return (start, end)
|
||||
|
||||
def is_slicing(self, word):
|
||||
m = re.match(r'\[[0-9\-]{0,}:[0-9\-]{0,}\]', word)
|
||||
return True if m else False
|
||||
|
||||
def get_t2c_indexes(self, t2c):
|
||||
text = self.text
|
||||
length = len(text)
|
||||
t2c_indexes = []
|
||||
for word, color in list(t2c.items()):
|
||||
# accurate mode
|
||||
if self.is_slicing(word):
|
||||
start, end = self.find_strat_and_end(text, word)
|
||||
start = length + start if start < 0 else start
|
||||
end = length + end if end < 0 else end
|
||||
t2c_indexes.append((start, end, color))
|
||||
continue
|
||||
for start, end in self.find_indexes(text, word):
|
||||
t2c_indexes.append((start, end, color))
|
||||
return sorted(t2c_indexes, key=lambda i: i[1])
|
||||
|
||||
def getfsw(self, **kwargs):
|
||||
font = kwargs['font'] if kwargs.__contains__('font') else DEFAULT_FONT
|
||||
slant = kwargs['slant'] if kwargs.__contains__('slant') else NORMAL
|
||||
weight = kwargs['weight'] if kwargs.__contains__('weight') else NORMAL
|
||||
return (font, slant, weight)
|
||||
|
||||
def getxywh(self, text, font, slant, weight, size):
|
||||
dir_name = consts.TEXT_DIR
|
||||
file_name = os.path.join(dir_name, 'temp')+'.svg'
|
||||
|
||||
temp_surface = cairo.SVGSurface(file_name, 1, 1)
|
||||
temp_context = cairo.Context(temp_surface)
|
||||
temp_context.set_font_size(size)
|
||||
if font != '':
|
||||
fs = self.str2slant(slant)
|
||||
fw = self.str2weight(weight)
|
||||
temp_context.select_font_face(font, fs, fw)
|
||||
x, y, w, h, dx, dy = temp_context.text_extents(text)
|
||||
return (x, y, w, h)
|
||||
|
||||
def get_space_w(self, font, size):
|
||||
x1, y1, w1, h1 = self.getxywh('a', font, NORMAL, NORMAL, size)
|
||||
x2, y2, w2, h2, = self.getxywh('aa', font, NORMAL, NORMAL, size)
|
||||
return w2 - w1*2
|
||||
|
||||
def has_multi_line(self, text):
|
||||
return True if re.search(r'\n', text) else False
|
||||
|
||||
def set_text_color(self, t2c):
|
||||
self.text2color(self.text, t2c=t2c)
|
||||
|
||||
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 text2color(self, text, **kwargs):
|
||||
for word, color in list(kwargs['t2c'].items()):
|
||||
# accurate mode
|
||||
if self.is_slicing(word):
|
||||
start, end = self.find_strat_and_end(text, word)
|
||||
self[start:end].set_color(color)
|
||||
continue
|
||||
for start, end in self.find_indexes(text, word):
|
||||
self[start:end].set_color(color)
|
||||
|
||||
def text2hash(self, text, **kwargs):
|
||||
ignores = [
|
||||
'color', 't2c',
|
||||
'fill_color', 'fill_opacity',
|
||||
'stroke_color', 'storke_opacity'
|
||||
]
|
||||
for ignore in ignores:
|
||||
if kwargs.__contains__(ignore):
|
||||
kwargs.pop(ignore)
|
||||
|
||||
id_str = text+str(kwargs)
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(id_str.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
|
||||
def text2styles(self, text, **kwargs):
|
||||
styles = []
|
||||
f0, s0, w0 = self.getfsw(**kwargs)
|
||||
|
||||
if kwargs.__contains__('t2f'):
|
||||
for word, font in list(kwargs['t2f'].items()):
|
||||
if self.is_slicing(word):
|
||||
start, end = self.find_strat_and_end(text, word)
|
||||
styles.append(TextStyle(start, end, font, s0, w0))
|
||||
for start, end in self.find_indexes(text, word):
|
||||
styles.append(TextStyle(start, end, font, s0, w0))
|
||||
|
||||
if kwargs.__contains__('t2s'):
|
||||
for word, slant in list(kwargs['t2s'].items()):
|
||||
if self.is_slicing(word):
|
||||
start, end = self.find_strat_and_end(text, word)
|
||||
styles.append(TextStyle(start, end, f0, slant, w0))
|
||||
for start, end in self.find_indexes(text, word):
|
||||
styles.append(TextStyle(start, end, f0, slant, w0))
|
||||
|
||||
if kwargs.__contains__('t2w'):
|
||||
for word, weight in list(kwargs['t2w'].items()):
|
||||
if self.is_slicing(word):
|
||||
start, end = self.find_strat_and_end(text, word)
|
||||
styles.append(TextStyle(start, end, f0, s0, weight))
|
||||
for start, end in self.find_indexes(text, word):
|
||||
styles.append(TextStyle(start, end, f0, s0, weight))
|
||||
|
||||
#Set All text styles(default font slant weight)
|
||||
styles = sorted(styles, key=lambda s: s.start)
|
||||
temp_styles = styles.copy()
|
||||
start = 0
|
||||
for style in styles:
|
||||
if style.start != start:
|
||||
temp_styles.append(TextStyle(start, style.start, f0, s0, w0))
|
||||
start = style.end
|
||||
if start != len(text):
|
||||
temp_styles.append(TextStyle(start, len(text), f0, s0, w0))
|
||||
styles = sorted(temp_styles, key=lambda s: s.start)
|
||||
|
||||
if self.has_multi_line(text):
|
||||
line_num = 0
|
||||
for start, end in self.find_indexes(text, '\n'):
|
||||
for style in styles:
|
||||
if style.line_num == -1:
|
||||
style.line_num = line_num
|
||||
if start < style.end:
|
||||
line_num += 1
|
||||
new_style = copy.copy(style)
|
||||
style.end = end
|
||||
new_style.start = end
|
||||
new_style.line_num = line_num
|
||||
styles.append(new_style)
|
||||
styles = sorted(styles, key=lambda s: s.start)
|
||||
break
|
||||
|
||||
return styles
|
||||
|
||||
def text2svg(self, text, **kwargs):
|
||||
font, slant, weight = self.getfsw(**kwargs)
|
||||
size = kwargs['size'] if kwargs.__contains__('size') else DEFAULT_SIZE
|
||||
lsh = kwargs['lsh'] if kwargs.__contains__('lsh') else DEFAULT_LSH
|
||||
#anti-aliasing
|
||||
size *= 10
|
||||
lsh *= 10
|
||||
|
||||
if font == '':
|
||||
print(NOT_SETTING_FONT_MSG)
|
||||
|
||||
dir_name = consts.TEXT_DIR
|
||||
hash_name = self.text2hash(text, **kwargs)
|
||||
file_name = os.path.join(dir_name, hash_name)+'.svg'
|
||||
if os.path.exists(file_name):
|
||||
return file_name
|
||||
|
||||
text_surface = cairo.SVGSurface(file_name, 300, 200)
|
||||
text_context = cairo.Context(text_surface)
|
||||
text_context.set_font_size(size)
|
||||
|
||||
styles = self.text2styles(text, **kwargs)
|
||||
last_width = 0
|
||||
last_ln = 0
|
||||
for style in styles:
|
||||
temp_text = text[style.start:style.end]
|
||||
sf = style.font
|
||||
ss = style.slant
|
||||
sw = style.weight
|
||||
sln = style.line_num if style.line_num != -1 else 0
|
||||
x1, y1, w1, h1 = self.getxywh(temp_text, sf, ss, sw, size)
|
||||
csf = self.str2slant(ss)
|
||||
csw = self.str2weight(sw)
|
||||
|
||||
text_context.select_font_face(sf, csf, csw)
|
||||
if sln != last_ln:
|
||||
last_width = 0
|
||||
last_ln = sln
|
||||
text_context.move_to(last_width-x1, lsh*sln)
|
||||
text_context.show_text(temp_text)
|
||||
|
||||
last_width += w1 + self.get_space_w(sf, size)
|
||||
|
||||
return file_name
|
Loading…
Add table
Reference in a new issue