mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
New Text Mobject
This commit is contained in:
parent
fd3721e050
commit
e9fa188d42
2 changed files with 149 additions and 257 deletions
|
@ -52,15 +52,20 @@ def initialize_directories(config):
|
||||||
if folder != "" and not os.path.exists(folder):
|
if folder != "" and not os.path.exists(folder):
|
||||||
os.makedirs(folder)
|
os.makedirs(folder)
|
||||||
|
|
||||||
DEFAULT_FONT = ''
|
|
||||||
DEFAULT_LSH = 1
|
|
||||||
DEFAULT_SIZE = 1
|
|
||||||
NOT_SETTING_FONT_MSG='''
|
NOT_SETTING_FONT_MSG='''
|
||||||
Warning:
|
Warning:
|
||||||
You haven't set DEFAULT_FONT.
|
You haven't set font.
|
||||||
If you are not using English, this may cause text rendering problem.
|
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').
|
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'
|
NORMAL = 'NORMAL'
|
||||||
ITALIC = 'ITALIC'
|
ITALIC = 'ITALIC'
|
||||||
OBLIQUE = 'OBLIQUE'
|
OBLIQUE = 'OBLIQUE'
|
||||||
|
|
|
@ -3,12 +3,13 @@ import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import manimlib.constants as consts
|
||||||
from manimlib.constants import *
|
from manimlib.constants import *
|
||||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||||
import manimlib.constants as consts
|
from manimlib.utils.config_ops import digest_config
|
||||||
|
|
||||||
|
|
||||||
class TextStyle(object):
|
class TextSetting(object):
|
||||||
def __init__(self, start, end, font, slant, weight, line_num=-1):
|
def __init__(self, start, end, font, slant, weight, line_num=-1):
|
||||||
self.start = start
|
self.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
|
@ -19,164 +20,85 @@ class TextStyle(object):
|
||||||
|
|
||||||
|
|
||||||
class Text(SVGMobject):
|
class Text(SVGMobject):
|
||||||
'''
|
|
||||||
Params:
|
|
||||||
-------
|
|
||||||
text ::
|
|
||||||
a str, the space(' ') in front or back and '\\t' will be ignored(when there is only one line)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 = {
|
CONFIG = {
|
||||||
"color": WHITE,
|
#Mobject
|
||||||
"fill_opacity": 1,
|
'color': WHITE,
|
||||||
"height": None,
|
'height': None,
|
||||||
|
#Text
|
||||||
|
'font': '',
|
||||||
|
'gradient': None,
|
||||||
|
'lsh': -1,
|
||||||
|
'size': 1,
|
||||||
|
'slant': NORMAL,
|
||||||
|
'weight': NORMAL,
|
||||||
|
't2c': {},
|
||||||
|
't2f': {},
|
||||||
|
't2g': {},
|
||||||
|
't2s': {},
|
||||||
|
't2w': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, text, **kwargs):
|
def __init__(self, text, **config):
|
||||||
self.text = text
|
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()
|
||||||
|
|
||||||
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
|
#anti-aliasing
|
||||||
self.scale(0.1)
|
self.scale(0.1)
|
||||||
|
|
||||||
def full2short(self, **kwargs):
|
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'):
|
if kwargs.__contains__('line_spacing_height'):
|
||||||
kwargs['lsh'] = kwargs.pop('line_spacing_height')
|
kwargs['lsh'] = kwargs.pop('line_spacing_height')
|
||||||
if kwargs.__contains__('text2color'):
|
if kwargs.__contains__('text2color'):
|
||||||
kwargs['t2c'] = kwargs.pop('text2color')
|
kwargs['t2c'] = kwargs.pop('text2color')
|
||||||
if kwargs.__contains__('text2font'):
|
if kwargs.__contains__('text2font'):
|
||||||
kwargs['t2f'] = kwargs.pop('text2font')
|
kwargs['t2f'] = kwargs.pop('text2font')
|
||||||
|
if kwargs.__contains__('text2gradient'):
|
||||||
|
kwargs['t2g'] = kwargs.pop('text2gradient')
|
||||||
if kwargs.__contains__('text2slant'):
|
if kwargs.__contains__('text2slant'):
|
||||||
kwargs['t2s'] = kwargs.pop('text2slant')
|
kwargs['t2s'] = kwargs.pop('text2slant')
|
||||||
if kwargs.__contains__('text2weight'):
|
if kwargs.__contains__('text2weight'):
|
||||||
kwargs['t2w'] = kwargs.pop('text2weight')
|
kwargs['t2w'] = kwargs.pop('text2weight')
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def find_indexes(self, text, word):
|
def set_color_by_t2c(self, t2c=None):
|
||||||
indexes = []
|
t2c = t2c if t2c else self.t2c
|
||||||
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()):
|
for word, color in list(t2c.items()):
|
||||||
# accurate mode
|
for start, end in self.find_indexes(word):
|
||||||
if self.is_slicing(word):
|
self[start:end].set_color(color)
|
||||||
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):
|
def set_color_by_t2g(self, t2g=None):
|
||||||
font = kwargs['font'] if kwargs.__contains__('font') else DEFAULT_FONT
|
t2g = t2g if t2g else self.t2g
|
||||||
slant = kwargs['slant'] if kwargs.__contains__('slant') else NORMAL
|
for word, gradient in list(t2g.items()):
|
||||||
weight = kwargs['weight'] if kwargs.__contains__('weight') else NORMAL
|
for start, end in self.find_indexes(word):
|
||||||
return (font, slant, weight)
|
self[start:end].set_color_by_gradient(*gradient)
|
||||||
|
|
||||||
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):
|
def str2slant(self, string):
|
||||||
if string == NORMAL:
|
if string == NORMAL:
|
||||||
|
@ -192,130 +114,95 @@ class Text(SVGMobject):
|
||||||
if string == BOLD:
|
if string == BOLD:
|
||||||
return cairo.FontWeight.BOLD
|
return cairo.FontWeight.BOLD
|
||||||
|
|
||||||
def text2color(self, text, **kwargs):
|
def text2hash(self):
|
||||||
for word, color in list(kwargs['t2c'].items()):
|
settings = self.font + self.slant + self.weight
|
||||||
# accurate mode
|
settings += str(self.t2f) + str(self.t2s) + str(self.t2w)
|
||||||
if self.is_slicing(word):
|
settings += str(self.lsh) + str(self.size)
|
||||||
start, end = self.find_strat_and_end(text, word)
|
id_str = self.text+settings
|
||||||
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'
|
|
||||||
]
|
|
||||||
for ignore in ignores:
|
|
||||||
if kwargs.__contains__(ignore):
|
|
||||||
kwargs.pop(ignore)
|
|
||||||
|
|
||||||
id_str = text+str(kwargs)
|
|
||||||
hasher = hashlib.sha256()
|
hasher = hashlib.sha256()
|
||||||
hasher.update(id_str.encode())
|
hasher.update(id_str.encode())
|
||||||
return hasher.hexdigest()[:16]
|
return hasher.hexdigest()[:16]
|
||||||
|
|
||||||
def text2styles(self, text, **kwargs):
|
def text2settings(self):
|
||||||
styles = []
|
settings = []
|
||||||
f0, s0, w0 = self.getfsw(**kwargs)
|
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))
|
||||||
|
|
||||||
if kwargs.__contains__('t2f'):
|
#Set All text settings(default font slant weight)
|
||||||
for word, font in list(kwargs['t2f'].items()):
|
fsw = [self.font, self.slant, self.weight]
|
||||||
if self.is_slicing(word):
|
settings.sort(key = lambda setting: setting.start)
|
||||||
start, end = self.find_strat_and_end(text, word)
|
temp_settings = settings.copy()
|
||||||
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
|
start = 0
|
||||||
for style in styles:
|
for setting in settings:
|
||||||
if style.start != start:
|
if setting.start != start:
|
||||||
temp_styles.append(TextStyle(start, style.start, f0, s0, w0))
|
temp_settings.append(TextSetting(start, setting.start, *fsw))
|
||||||
start = style.end
|
start = setting.end
|
||||||
if start != len(text):
|
if start != len(self.text):
|
||||||
temp_styles.append(TextStyle(start, len(text), f0, s0, w0))
|
temp_settings.append(TextSetting(start, len(self.text), *fsw))
|
||||||
styles = sorted(temp_styles, key=lambda s: s.start)
|
settings = sorted(temp_settings, key = lambda setting: setting.start)
|
||||||
|
|
||||||
if self.has_multi_line(text):
|
if re.search(r'\n', self.text):
|
||||||
line_num = 0
|
line_num = 0
|
||||||
for start, end in self.find_indexes(text, '\n'):
|
for start, end in self.find_indexes('\n'):
|
||||||
for style in styles:
|
for setting in settings:
|
||||||
if style.line_num == -1:
|
if setting.line_num == -1:
|
||||||
style.line_num = line_num
|
setting.line_num = line_num
|
||||||
if start < style.end:
|
if start < setting.end:
|
||||||
line_num += 1
|
line_num += 1
|
||||||
new_style = copy.copy(style)
|
new_setting = copy.copy(setting)
|
||||||
style.end = end
|
setting.end = end
|
||||||
new_style.start = end
|
new_setting.start = end
|
||||||
new_style.line_num = line_num
|
new_setting.line_num = line_num
|
||||||
styles.append(new_style)
|
settings.append(new_setting)
|
||||||
styles = sorted(styles, key=lambda s: s.start)
|
settings.sort(key = lambda setting: setting.start)
|
||||||
break
|
break
|
||||||
|
|
||||||
return styles
|
for setting in settings:
|
||||||
|
if setting.line_num == -1:
|
||||||
|
setting.line_num = 0
|
||||||
|
|
||||||
def text2svg(self, text, **kwargs):
|
return settings
|
||||||
font, slant, weight = self.getfsw(**kwargs)
|
|
||||||
size = kwargs['size'] if kwargs.__contains__('size') else DEFAULT_SIZE
|
def text2svg(self):
|
||||||
lsh = kwargs['lsh'] if kwargs.__contains__('lsh') else DEFAULT_LSH
|
|
||||||
#anti-aliasing
|
#anti-aliasing
|
||||||
size *= 10
|
size = self.size * 10
|
||||||
lsh *= 10
|
lsh = self.lsh * 10
|
||||||
|
|
||||||
if font == '':
|
if self.font == '':
|
||||||
print(NOT_SETTING_FONT_MSG)
|
print(NOT_SETTING_FONT_MSG)
|
||||||
|
|
||||||
dir_name = consts.TEXT_DIR
|
dir_name = consts.TEXT_DIR
|
||||||
hash_name = self.text2hash(text, **kwargs)
|
hash_name = self.text2hash()
|
||||||
file_name = os.path.join(dir_name, hash_name)+'.svg'
|
file_name = os.path.join(dir_name, hash_name)+'.svg'
|
||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
return file_name
|
return file_name
|
||||||
|
|
||||||
text_surface = cairo.SVGSurface(file_name, 300, 200)
|
surface = cairo.SVGSurface(file_name, 600, 400)
|
||||||
text_context = cairo.Context(text_surface)
|
context = cairo.Context(surface)
|
||||||
text_context.set_font_size(size)
|
context.set_font_size(size)
|
||||||
|
context.move_to(START_X, START_Y)
|
||||||
|
|
||||||
styles = self.text2styles(text, **kwargs)
|
settings = self.text2settings()
|
||||||
last_width = 0
|
offset_x = 0
|
||||||
last_ln = 0
|
last_line_num = 0
|
||||||
for style in styles:
|
for setting in settings:
|
||||||
temp_text = text[style.start:style.end]
|
font = setting.font
|
||||||
sf = style.font
|
slant = self.str2slant(setting.slant)
|
||||||
ss = style.slant
|
weight = self.str2weight(setting.weight)
|
||||||
sw = style.weight
|
text = self.text[setting.start:setting.end].replace('\n', ' ')
|
||||||
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)
|
context.select_font_face(font, slant, weight)
|
||||||
if sln != last_ln:
|
if setting.line_num != last_line_num:
|
||||||
last_width = 0
|
offset_x = 0
|
||||||
last_ln = sln
|
last_line_num = setting.line_num
|
||||||
text_context.move_to(last_width-x1, lsh*sln)
|
context.move_to(START_X+offset_x, START_Y + lsh*setting.line_num)
|
||||||
text_context.show_text(temp_text)
|
context.show_text(text)
|
||||||
|
offset_x += context.text_extents(text)[4]
|
||||||
last_width += w1 + self.get_space_w(sf, size)
|
|
||||||
|
|
||||||
return file_name
|
return file_name
|
Loading…
Add table
Reference in a new issue