New Text Mobject

This commit is contained in:
xy-23 2019-08-12 09:35:05 +08:00
parent fd3721e050
commit e9fa188d42
2 changed files with 149 additions and 257 deletions

View file

@ -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'

View file

@ -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,165 +20,86 @@ 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)
kwargs = self.full2short(**kwargs) if self.t2c:
file_name = self.text2svg(text, **kwargs) self.set_color_by_t2c()
SVGMobject.__init__(self, file_name=file_name, **kwargs) if self.gradient:
if kwargs.__contains__('t2c'): self.set_color_by_gradient(*self.gradient)
self.text2color(text, **kwargs) if self.t2g:
self.set_color_by_t2g()
#anti-aliasing #anti-aliasing
self.scale(0.1) 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)]
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 = [] indexes = []
if re.match(r'\[\[[0-9\-]{0,}:[0-9\-]{0,}\]\]', word): index = self.text.find(word)
word = word[1:-1]
index = text.find(word)
while index != -1: while index != -1:
indexes.append((index, index+len(word))) indexes.append((index, index + len(word)))
index = text.find(word, index+len(word)) index = self.text.find(word, index + len(word))
return indexes return indexes
def find_strat_and_end(self, text, word): def full2short(self, config):
m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word) for kwargs in [config, self.CONFIG]:
start = int(m.group(1)) if m.group(1) != '' else 0 if kwargs.__contains__('line_spacing_height'):
end = int(m.group(2)) if m.group(2) != '' else len(text) kwargs['lsh'] = kwargs.pop('line_spacing_height')
return (start, end) 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 is_slicing(self, word): def set_color_by_t2c(self, t2c=None):
m = re.match(r'\[[0-9\-]{0,}:[0-9\-]{0,}\]', word) t2c = t2c if t2c else self.t2c
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:
return cairo.FontSlant.NORMAL return cairo.FontSlant.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)):
if kwargs.__contains__('t2f'): fsw = [self.font, self.slant, self.weight]
for word, font in list(kwargs['t2f'].items()): if t2x[i]:
if self.is_slicing(word): for word, x in list(t2x[i].items()):
start, end = self.find_strat_and_end(text, word) for start, end in self.find_indexes(word):
styles.append(TextStyle(start, end, font, s0, w0)) fsw[i] = x
for start, end in self.find_indexes(text, word): settings.append(TextSetting(start, end, *fsw))
styles.append(TextStyle(start, end, font, s0, w0))
#Set All text settings(default font slant weight)
if kwargs.__contains__('t2s'): fsw = [self.font, self.slant, self.weight]
for word, slant in list(kwargs['t2s'].items()): settings.sort(key = lambda setting: setting.start)
if self.is_slicing(word): temp_settings = settings.copy()
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
for setting in settings:
if setting.line_num == -1:
setting.line_num = 0
return styles return settings
def text2svg(self, text, **kwargs): def text2svg(self):
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 #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
surface = cairo.SVGSurface(file_name, 600, 400)
context = cairo.Context(surface)
context.set_font_size(size)
context.move_to(START_X, START_Y)
text_surface = cairo.SVGSurface(file_name, 300, 200) settings = self.text2settings()
text_context = cairo.Context(text_surface) offset_x = 0
text_context.set_font_size(size) last_line_num = 0
for setting in settings:
styles = self.text2styles(text, **kwargs) font = setting.font
last_width = 0 slant = self.str2slant(setting.slant)
last_ln = 0 weight = self.str2weight(setting.weight)
for style in styles: text = self.text[setting.start:setting.end].replace('\n', ' ')
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) 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