3b1b-manim/manimlib/mobject/svg/text_mobject.py
2019-08-06 21:05:37 +08:00

321 lines
11 KiB
Python

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 '\\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 = {
"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'
]
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